# Core concepts (/concepts)

Learn about the key building blocks of Inkeep - Agents, Sub Agents, tools, data components, and more.



## Agents

In Inkeep, an **Agent** is the top-level entity you can interface with via conversational experiences (chat) or trigger programmatically (via API).

Under the hood, an Agent is made up of one or more **Sub Agents** that work together to respond to a user or complete a task.

## Tools

When you send a message to an Agent, it is first received by a **Default Sub Agent** that decides what to do next.

In a simple Agent, there may be only one Sub Agent with a few tools available to it.

**Tools** are actions that a Sub Agent can take, like looking up information or performing a task on apps and APIs.

In Inkeep, tools can be added to Sub Agents as:

* **MCP Servers**: Connect to external services and APIs via the Model Context Protocol. You can:

  * **Connect to Native MCP servers** provided directly by SaaS vendors (no building required)
  * **Access Composio's platform** for 10,000+ out-of-box MCP servers for popular services (no building required)
  * **Use Gram** to convert OpenAPI specs into MCP servers
  * **Build and deploy Custom servers** for your own APIs and business logic

  Register any of these with their associated **Credentials** for your Agents to use.
* **Function Tools**: Custom JavaScript functions that Agents can execute directly without the need for standing up an MCP server.

Typically, you want a Sub Agent to handle narrow, well-defined tasks. As a general rule of thumb, keep Sub Agents to be using 5-7 related tools at a time.

## Sub Agent relationships

When your scenario gets complex, it can be useful to break up your logic into multiple Sub Agents that are specialized in specific parts of your task or workflow. This is often referred to as a "Multi-agent" system.

A Sub Agent can be configured to:

* **Transfer** control of the chat to another Sub Agent. When a transfer happens, the receiving Sub Agent becomes the primary driver of the thread and can respond to the user directly.
* **Delegate** a subtask for another ('child') Sub Agent to do and wait for its response before proceeding with the next step. A child Sub Agent *cannot* respond directly to a user.

## Sub Agent 'turn'

When it's a Sub Agent's turn, it can choose to:

1. Send an update message to the user
2. Call a tool to collect information or take an action
3. Transfer or delegate to another Sub Agent

An Agent's execution stays in this loop until one of the Sub Agents chooses to respond to the user with a final result.

<Note>
  Sub Agents in Inkeep are designed to respond to the user as a single, cohesive unit by default.
</Note>

## Chatting with an Agent

<>
  You can talk to an Inkeep Agent in a few ways, including:

  * **UI Chat Components**: Drop-in React components for chat UIs with built-in streaming and rich UI customization. See [`agents-ui`](/talk-to-your-agents/react/chat-button).
  * **As an MCP server**: Use your Inkeep Agent as if was an MCP Server. Allows you to connect it to any MCP client, like Claude, ChatGPT, Claude and other Agents. See [MCP server](/talk-to-your-agents/mcp-server).
  * **Via API (Vercel format)**: An API that streams responses over server-side events (SSE). Use from any language/runtime, including the Vercel's `useChat` and AI Element primitives for custom UIs. See [API (Vercel format)](/talk-to-your-agents/api).
  * **Via API (A2A format)**: An API that follows the Agent-to-Agent ('A2A') JSON-RPC protocol. Great for when combining Inkeep with different Agent frameworks that support the A2A format. See [A2A protocol](/talk-to-your-agents/a2a).
  * **Via Webhook Triggers**: Create webhook endpoints that allow external services (GitHub, Slack, Stripe, etc.) to invoke your Agents. See [Triggers](/talk-to-your-agents/triggers).
  * **Slack**: Interact with agents directly in Slack using `@Inkeep` and slash commands. Available for Enterprise. See [Slack](/talk-to-your-agents/slack/overview).

  <Cards>
    <Card title="React UI" icon="LuMessageSquare" href="/talk-to-your-agents/react/chat-button">
      Drop-in chat components for React apps with streaming and rich UI.
    </Card>

    <Card title="API (Vercel format)" icon="LuNetwork" href="/talk-to-your-agents/chat-api">
      POST /api/chat, SSE (text/event-stream), x-vercel-ai-data-stream: v2.
    </Card>

    <Card title="A2A protocol" icon="LuNetwork" href="/talk-to-your-agents/a2a">
      JSON-RPC messages at /agents/a2a with blocking and streaming modes.
    </Card>

    <Card title="MCP server" icon="LuServer" href="/talk-to-your-agents/mcp-server">
      HTTP JSON-RPC endpoint at /v1/mcp with session header management.
    </Card>

    <Card title="Triggers" icon="LuZap" href="/talk-to-your-agents/triggers">
      Webhook and scheduled triggers for event-driven Agent invocation.
    </Card>

    <Card title="Slack" icon="LuMessageSquare" href="/talk-to-your-agents/slack/overview">
      Mention `@Inkeep` or use `/inkeep` commands to talk to agents in Slack.
    </Card>
  </Cards>
</>

## Triggers

**Triggers** are webhook endpoints that allow external services to invoke your Agents. When a webhook is received, the payload is validated, transformed into a message, and used to start a new conversation.

Triggers are useful for:

* **Event-driven workflows** - Respond to events from external services like GitHub, Slack, or Stripe
* **Third-party integrations** - Connect any service that can send HTTP webhooks to your Agents
* **Automated pipelines** - Kick off Agent tasks from CI/CD, cron jobs, or other automation systems

Each trigger can be configured with:

* **Input validation** - JSON Schema to validate incoming payloads
* **Message templates** - Transform webhook payloads into natural language messages using `{{placeholder}}` syntax
* **Authentication** - API keys, bearer tokens, or basic auth to secure the endpoint
* **Signature verification** - HMAC-SHA256 verification for services like GitHub that sign webhooks

When a webhook is received, the trigger creates a new conversation and invokes the Agent asynchronously, returning immediately with an invocation ID for tracking.

<Cards>
  <Card title="Triggers" icon="LuZap" href="/talk-to-your-agents/triggers">
    Learn how triggers work and when to use them.
  </Card>

  <Card title="TypeScript SDK" icon="LuCode" href="/typescript-sdk/triggers/overview">
    Define triggers in code with the SDK.
  </Card>
</Cards>

## Authentication & API Keys

You can authenticate with your Agent using:

* **API Keys**: Securely hashed keys that are scoped to specific Agents
* **Development Mode**: No API key required, perfect for local development and testing
* **Bypass Secrets**: For internal services and infrastructure that need direct access

API keys are the recommended approach for production server-to-server use, providing secure, scoped access to your Agents without exposing secrets to end-users. For app-based integrations such as web clients and authenticated end-user experiences, use [App Credentials](/talk-to-your-agents/app-credentials).

## Agent replies with Structured Data

Sometimes, you want your Agent to reply not in plain text but with specific types of well-defined information, often called 'Structured Outputs' (JSON).

With Inkeep, there are a few ways to do this:

* **Data Components**: Structured Outputs that Sub Agents can output in their messages so they can render rich, interactive UIs (lists, buttons, forms, etc.) or convey structured information.
* **Artifacts**: A Sub Agent can save information from a **tool call result** as an artifact. Artifact schemas define **preview fields** (immediately available in the agent's context and streamed to clients) and non-preview fields (persisted in storage but kept out of context by default). Agents can reference artifacts in their responses, pass them to tools for full data access, or retrieve the complete artifact on demand when they need the non-preview fields. See [Artifact Components](/typescript-sdk/structured-outputs/artifact-components) for details.
* **Status Updates**: Real-time progress updates that can be plain text or Structured Outputs that can be used to keep users informed about what the Sub Agent is doing during longer operations.

## Passing context to Sub Agents

Beyond using Tools to fetch information, Sub Agents also receive information via:

* **Headers**: In the API request to an Agent, the calling application can include headers for a Sub Agent. Learn more [here](/typescript-sdk/headers).
* **Context Fetchers**: Can be configured for an Agent so that at the beginning of a conversation, an API call is automatically made to an external service to get information that is then made available to any Sub Agent. For example, your Headers may include a `user-id`, which can be used to auto-fetch information from a CRM about the user for any Sub Agent to use.

Headers and fetched context can then be referenced explicitly as `{{variables}}` in Sub Agent prompts. Learn more [here](/typescript-sdk/headers).

## Ways to build

Quick reference to the key docs for building with the Visual Builder or the TypeScript SDK.

<Tabs>
  <Tab title="Visual Builder">
    <Cards>
      <Card title="MCP Servers" icon="LuServer" href="/visual-builder/tools/mcp-servers">
        Configure and manage MCP servers for your Sub Agents.
      </Card>

      <Card title="Agents" icon="LuSpline" href="/visual-builder/sub-agents">
        Create and manage Agents visually.
      </Card>

      <Card title="Data Components" icon="LuBlocks" href="/visual-builder/structured-outputs/data-components">
        Build rich UI elements Sub Agents can render in conversations.
      </Card>

      <Card title="Artifact Components" icon="TbInputSpark" href="/visual-builder/structured-outputs/artifact-components">
        Define structured outputs generated by tools or Sub Agents.
      </Card>

      <Card title="Status Components" icon="LuClock" href="/visual-builder/structured-outputs/status-components">
        Show progress updates during longer operations.
      </Card>

      <Card title="Credentials" icon="LuKey" href="/visual-builder/tools/credentials">
        Manage secrets and auth for MCP servers.
      </Card>

      <Card title="Project Management" icon="LuFolder" href="/visual-builder/project-management">
        Organize agents, MCP Servers, and other entities in Projects.
      </Card>
    </Cards>
  </Tab>

  <Tab title="TypeScript SDK">
    <Cards>
      <Card title="Sub Agent Settings" icon="LuUser" href="/typescript-sdk/agent-settings">
        Configure Sub Agents with prompts, tools, and data components.
      </Card>

      <Card title="MCP Servers" icon="LuHammer" href="/typescript-sdk/tools/mcp-servers">
        Add tools as MCP servers.
      </Card>

      <Card title="Function Tools" icon="LuCode" href="/typescript-sdk/tools/function-tools">
        Create custom JavaScript functions that run in secure sandboxes.
      </Card>

      <Card title="Sub Agent Relationships" icon="LuUsers" href="/typescript-sdk/agent-relationships">
        Define how Sub Agents transfer and delegate tasks.
      </Card>

      <Card title="Data Components" icon="LuBlocks" href="/typescript-sdk/structured-outputs/data-components">
        Build custom UI elements Sub Agents can render.
      </Card>

      <Card title="Artifact Components" icon="TbInputSpark" href="/typescript-sdk/structured-outputs/artifact-components">
        Create structured outputs from tools or Sub Agents.
      </Card>

      <Card title="Status Updates" icon="LuClock" href="/typescript-sdk/structured-outputs/status-updates">
        Provide real-time progress updates.
      </Card>

      <Card title="Context Fetchers" icon="LuCirclePlus" href="/typescript-sdk/context-fetchers">
        Dynamically fetch and cache external context.
      </Card>

      <Card title="Credentials" icon="LuKey" href="/typescript-sdk/credentials/overview">
        Store and retrieve credentials for MCP tools.
      </Card>

      <Card title="Triggers" icon="LuWebhook" href="/typescript-sdk/triggers/overview">
        Create webhook endpoints for external services.
      </Card>
    </Cards>
  </Tab>
</Tabs>

The Visual Builder and TypeScript SDK work seamlessly together—define your Sub Agents in code, push them to the Visual Builder, and iterate visually.

## Projects

You can organize your related MCP Servers, Credentials, Agents, and more into **Projects**. A Project is generally used to represent a set of related scenarios.

For example, you may create one Project for your support team that has all the MCP servers and Agents related to customer support.

## CLI: Push and pull

The Inkeep CLI bridges your TypeScript SDK project and the Visual Builder.

Run the following from your project (the folder that contains your `inkeep.config.ts`) which has an `index.ts` file that exports a project.

* **Push (code → Builder)**: Sync locally defined agents, Sub Agents, tools, and settings from your SDK project into the Visual Builder.

```bash
inkeep push
```

* **Pull (Builder → code)**: Fetch your project from the Visual Builder back into your SDK project. By default, the CLI will LLM-assist in updating your local TypeScript files to reflect Builder changes.

```bash
inkeep pull
```

<Note>
  Push and pull operate at the project level (not individual agents). Define agents in your project and push/pull the whole project.
</Note>

See the [CLI Reference](/typescript-sdk/cli-reference) for full command details.

## Deployment

Once you've built your Agents, you can deploy them using:

<Cards>
  <Card title="Deploy with Docker" icon="LuContainer" href="/self-hosting/docker">
    Self-host your Agents using Docker for full control and flexibility.
  </Card>

  <Card title="Deploy to Vercel" icon="LuRocket" href="/self-hosting/vercel">
    Deploy your Agents to Vercel for easy serverless hosting.
  </Card>
</Cards>

## Architecture

The Inkeep Agent framework is composed of several key services and libraries that work together:

* **agents-api**: An API that handles configuration of Agents, Sub Agents, MCP Servers, Credentials, and Projects with a REST API.
* **agents-manage-ui**: Visual Builder web interface for creating and managing Agents. Writes to the `agents-api`.
* **agents-sdk**: TypeScript SDK (`@inkeep/agents-sdk`) for declaratively defining Agents and custom tools in code. Writes to `agents-api`.
* **agents-cli**: Includes various handy utilities, including `inkeep push` and `inkeep pull` which sync your TypeScript SDK code with the Visual Builder.
* **agents-ui**: A UI component library of chat interfaces for embedding rich, dynamic conversational AI experiences in web apps.


# The No-Code + Code Agent Builder (/overview)

Inkeep is a platform for building Agent Chat Assistants and AI Workflows.



With Inkeep, you can build AI Agents with a **No-Code Visual Builder** and **Developer SDK**. Agents can be edited in either with **full 2-way sync**, so technical and non-technical teams can create and manage their Agents in one platform.

## Two ways to build

### No-Code Visual Builder

A drag-and-drop canvas so any team can create and own the Agents they care about.

<Image src="/gifs/drag-n-drop.gif" alt="No-Code Agent Builder demo" />

### TypeScript Agents SDK

A code-first framework so engineering teams can build with the tools they expect.

```typescript
import { agent, subAgent } from "@inkeep/agents-sdk";
import { consoleMcp } from "./mcp";

const helloAgent = subAgent({
  id: "hello-agent",
  name: "Hello Agent",
  description: "Says hello",
  canUse: () => [consoleMcp],
  prompt: `Reply to the user and console log "hello world" with fun variations like h3llo world`,
});

export const basicAgent = agent({
  id: "basic-agent",
  name: "Basic Agent",
  description: "A basic agent",
  defaultSubAgent: helloAgent,
  subAgents: () => [helloAgent],
});
```

The **Visual Builder and TypeScript SDK are fully interoperable**: your technical and non-technical teams can edit and manage Agents in either format and switch or collaborate with others at any time.

## Use cases

Inkeep Agents can operate as **Agentic AI Chat Assistants**, for example:

* a customer experience agent for help centers, technical docs, or in-app experiences
* an internal copilot to assist your support, sales, marketing, ops, and other teams

Agents can also be used for **Agentic Workflow Automation** like:

* Creating and updating knowledge bases, documentation, and blogs
* Updating CRMs, triaging helpdesk tickets, and tackling repetitive tasks

## Platform Overview

**Inkeep Open Source** includes:

* A Visual Builder & TypeScript SDK with 2-way sync
* Multi-agent architecture to support teams of agents
* MCP Tools with credentials management
* A UI component library for dynamic chat experiences
* Triggering Agents via MCP, A2A, Webhooks, & Vercel SDK APIs
* Observability via a Traces UI & OpenTelemetry
* Easy deployment to Vercel and using Docker

Interested in a managed platform? Sign up for a demo for [Inkeep Enterprise](https://inkeep.com/demo?cta_id=docs_nav) and learn more [here](https://inkeep.com/enterprise). You can view a full feature comparison [here](/pricing).

## Our Approach

Inkeep is designed to be extensible and open: you can use the LLM provider of your choice, use Agents via open protocols, and with a [fair-code](/community/license) license and great devex, easily deploy and self-host Agents in your own infra.

[Join our community](https://docs.inkeep.com/community/inkeep-community) to get support, stay up to date, and share feedback.

## Next Steps

<Cards>
  <Card title="Follow the <1min Quick Start" icon="LuZap" href="/get-started/quick-start">
    Get started with the Visual Builder and TypeScript SDK in under 5 minutes.
  </Card>

  <Card title="Conceptual Overview" icon="LuLightbulb" href="/concepts">
    Learn about the key concepts of building Agents with Inkeep.
  </Card>
</Cards>


# Pricing (/pricing)

Learn about Inkeep's pricing plans and features



Inkeep offers two ways to get started: **Open Source** (free forever), and **Enterprise** (managed platform with dedicated support).

<OptionCards>
  <OptionCard title="Open Source" icon="LuCode" href="/get-started/quick-start" subtitle="Everything you need to create AI Agents" cta="Quick Start">
    * Visual Builder & SDK
    * MCP Servers & Tools
    * Observability & UI Lib
    * Use with Claude/Cursor
    * Deploy to Vercel or Docker
  </OptionCard>

  <OptionCard title="Enterprise" icon="LuBuilding" href="https://inkeep.com/demo" subtitle="Everything in Open Source plus:" badge="Contact Us" highlighted cta="Schedule a Demo">
    * Dedicated forward deployed engineer
    * Unified AI Search (Managed RAG)
    * Use from Slack & Support Platforms
    * PII removal and data controls
    * Cloud Hosting & User Management
    * Trainings, enablement, and support
  </OptionCard>
</OptionCards>

## Feature Comparison

### Building Agents

| Feature                          | Open Source | Enterprise |
| -------------------------------- | :---------: | :--------: |
| No-Code Visual Builder           |      ✓      |      ✓     |
| Agent Developer SDK (TypeScript) |      ✓      |      ✓     |
| 2-way Sync: Edit in Code or UI   |      ✓      |      ✓     |

### Core Framework

| Feature                                                | Open Source | Enterprise |
| ------------------------------------------------------ | :---------: | :--------: |
| Take actions on any MCP Server, App, or API            |      ✓      |      ✓     |
| Multi-agent Architecture (Teams of Agents)             |      ✓      |      ✓     |
| Agent Credential and Permissions Management            |      ✓      |      ✓     |
| Agent Traces available in UI and OTEL                  |      ✓      |      ✓     |
| Talk to Agents via A2A, MCP, and Vercel AI SDK formats |      ✓      |      ✓     |

### Talk to Your Agents (Out of the Box)

| Feature                                            | Open Source | Enterprise |
| -------------------------------------------------- | :---------: | :--------: |
| With Claude, ChatGPT, and Cursor                   |      ✓      |      ✓     |
| With Slack, Discord, and Teams integrations        |      —      |      ✓     |
| With Zendesk, Salesforce, and support integrations |      —      |      ✓     |

### Building Agent UIs

| Feature                                             | Open Source | Enterprise |
| --------------------------------------------------- | :---------: | :--------: |
| Agent Messages with Custom UIs (forms, cards, etc.) |      ✓      |      ✓     |
| Custom UIs using Vercel AI SDK format               |      ✓      |      ✓     |
| Out-of-box Chat Components (React, JS)              |      ✓      |      ✓     |
| Answers with Inline Citations                       |      ✓      |      ✓     |

### Unified AI Search (Managed RAG)

| Feature                                              | Open Source | Enterprise |
| ---------------------------------------------------- | :---------: | :--------: |
| Real-time fetch from databases, APIs, and the web    |      ✓      |      ✓     |
| Public sources ingestion (docs, help center, etc.)   |      —      |      ✓     |
| Private sources ingestion (Notion, Confluence, etc.) |      —      |      ✓     |
| Optimized Retrieval and Search (Managed RAG)         |      —      |      ✓     |
| Semantic Search                                      |      —      |      ✓     |

### Insights & Analytics

| Feature                            | Open Source | Enterprise |
| ---------------------------------- | :---------: | :--------: |
| AI Reports on Knowledge Gaps       |      —      |      ✓     |
| AI Reports on Product Feature Gaps |      —      |      ✓     |

### Authentication and Authorization

| Feature                   | Open Source | Enterprise |
| ------------------------- | :---------: | :--------: |
| Single Sign-on            |      —      |      ✓     |
| Role-Based Access Control |      —      |      ✓     |
| Audit Logs                |      —      |      ✓     |

### Security

| Feature                               | Open Source | Enterprise |
| ------------------------------------- | :---------: | :--------: |
| PII Removal                           |      —      |      ✓     |
| Uptime and Support SLAs               |      —      |      ✓     |
| SOC II Type II and Pentest Reports    |      —      |      ✓     |
| GDPR, HIPAA, DPA, and Infosec Reviews |      —      |      ✓     |

### Deployment

| Feature       | Open Source |           Enterprise          |
| ------------- | :---------: | :---------------------------: |
| Hosting Types | Self-hosted | Cloud, Hybrid, or Self-hosted |
| Support Type  |  Community  |   Dedicated Engineering Team  |

### Forward Deployed Engineer Program

| Feature                                    | Open Source | Enterprise |
| ------------------------------------------ | :---------: | :--------: |
| Dedicated Architect and AI Agents Engineer |      —      |      ✓     |
| 1:1 Office Hours and Trainings             |      —      |      ✓     |
| Structured Pilot                           |      —      |      ✓     |


# Troubleshooting Guide (/troubleshooting)

Learn how to diagnose and resolve issues when something breaks in your Inkeep agent system.



## Overview

This guide provides a structured methodology for debugging problems across different components of your agent system.

## Step 1: Check the Timeline

The timeline is your first stop for understanding what happened during a conversation or agent execution. Navigate to the **Traces** sections to view in depth details per conversation. Within each conversation, you'll find an **error card** that is clickable whenever something goes wrong during agent execution.

### What to Look For

* **Execution flow**: Review the sequence of agent actions and tool calls
* **Timing**: Check for delays or bottlenecks in the execution
* **Agent transitions**: Verify that transfers and delegations happened as expected
* **Tool usage**: Confirm that tools were called correctly and returned expected results
* **Error cards**: Look for red error indicators in the timeline and click to view detailed error information

### Error Cards in the Timeline

Clicking on this error card reveals:

* **Error type**: The specific category of error (e.g., "Agent Generation Error")
* **Exception stacktrace**: The complete stack trace showing exactly where the error occurred in the code

This detailed error information helps you pinpoint exactly what went wrong and where in your agent's execution chain.

<>
  ### Copy Trace for Debugging

  The `Copy Trace` button in the timeline view allows you to export the entire conversation trace as JSON. This is particularly useful for offline analysis and debugging complex flows.

  <Image src="/images/copy-trace.png" alt="Copy Trace button in the timeline view for exporting conversation traces" />

  #### What's Included in the Trace Export

  When you click `Copy Trace`, the system exports a JSON object containing:

  ```json
  {
    "metadata": {
      "conversationId": "unique-conversation-id",
      "traceId": "distributed-trace-id",
      "agentId": "agent-identifier",
      "agentName": "Agent Name",
      "exportedAt": "2025-10-14T12:00:00.000Z"
    },
    "timing": {
      "startTime": "2025-10-14T11:59:00.000Z",
      "endTime": "2025-10-14T12:00:00.000Z",
      "durationMs": 60000
    },
    "timeline": [
      // Array of all activities with complete details:
      // - Agent messages and responses
      // - Tool calls and results
      // - Agent transfers
      // - Artifact information
      // - Execution context
    ]
  }
  ```

  #### How to Use Copy Trace

  1. Navigate to the **Traces** section in the management UI
  2. Open the conversation you want to debug
  3. Click the **Copy Trace** button at the top of the timeline
  4. The complete trace JSON is copied to your clipboard
  5. Paste it into your preferred tool for analysis

  This exported trace contains all the activities shown in the timeline, making it easy to share complete execution context with team members or support.
</>

## Step 2: Check SigNoz

SigNoz provides distributed tracing and observability for your agent system, offering deeper insights when the built-in timeline isn't sufficient.

### Accessing SigNoz from the Timeline

You can easily access SigNoz directly from the timeline view. In the **Traces** section, click on any activity in the conversation timeline to view its details. Within the activity details, you'll find a **"View in SigNoz"** button that takes you directly to the corresponding span in SigNoz for deeper analysis.

### What SigNoz Shows

* **Distributed traces**: End-to-end request flows across services
* **Performance metrics**: Response times, throughput, and error rates

### Key Metrics to Monitor

* **Agent response times**: How long each agent takes to process requests
* **Tool execution times**: Performance of MCP servers and external APIs
* **Error rates**: Frequency and types of failures

## Agent Stopped Unexpectedly

### StopWhen Limits Reached

If your agent stops mid-conversation, it may have hit a configured stopWhen limit:

* **Transfer limit reached**: Check `transferCountIs` on your Agent or Project - agent stops after this many transfers between Sub Agents
* **Step limit reached**: Check `stepCountIs` on your Sub Agent or Project - execution stops after this many tool calls + LLM responses

**How to diagnose:**

* Check the timeline for the last activity before stopping
* Look for messages indicating limits were reached
* Review your stopWhen configuration in Agent/Project settings

**How to fix:**

* Increase the limits if legitimate use case requires more steps/transfers
* Optimize your agent flow to use fewer transfers
* Investigate if agent is stuck in a loop (limits working as intended)

See [Configuring StopWhen](/typescript-sdk/agent-settings#configuring-stopwhen) for more details.

## Local environment not starting

If `pnpm dev` fails or services aren't reachable, the most common cause is that `pnpm setup-dev` hasn't been run (or Docker containers have stopped).

**How to fix:**

1. Ensure Docker Desktop (or Docker daemon) is running
2. Run `pnpm setup-dev` — it will start any stopped databases, apply pending migrations, and ensure the admin user exists
3. Run `pnpm dev` again

For the [quickstart template](/get-started/quick-start), the same applies: run `pnpm setup-dev` from the workspace root before `pnpm dev`.

### Port conflicts

If `pnpm setup-dev` reports that ports 5432, 5433, or 5434 are in use by another Docker Compose project, stop that project first:

```bash
docker compose -p <project-name> down
```

The setup script tells you the exact project name and command. After stopping the conflicting project, re-run `pnpm setup-dev`.

## Check service logs (local development)

When running `pnpm dev` from your [quickstart workspace](/get-started/quick-start), you will see an interactive terminal interface. This interface allows you to inspect the logs of each running service. You can navigate between services using the up and down arrow keys.

<img src="/images/agents-quickstart-pnpm-dev.png" alt="Service logs in local development" style={{ borderRadius: '8px' }} />

* The `service-info` tab displays the health of each running service.
* The `manage-api` tab contains logs for all database operations. This is useful primarily for debugging issues with [`inkeep push`](/typescript-sdk/cli-reference#inkeep-push).
* The `run-api` tab contains logs for all agent execution and tool calls. This is useful for debugging issues with your agent's behavior.
* The `mcp` tab contains logs for your [custom MCP servers](/guides/mcp-servers/custom-mcp-servers).
* The `dashboard` tab displays logs for the [Visual Builder](/visual-builder/sub-agents) dashboard.

To terminate the running services, click press `q` or `esc` in the terminal.

## CLI Issues

### Login fails or browser shows "connection refused"

* Verify the dashboard is running (`inkeep dev` or check Docker services)
* Check the manage UI URL in your profile: `inkeep profile current`
* Common cause: profile has a wrong port — re-create with `inkeep profile add`

### "Device code expired" during login

This usually means the manage UI was unreachable when you tried to approve the device code.

* Check: `inkeep profile current` — verify the **Manage UI** URL is correct and accessible in your browser
* For local development, the default manage UI runs on `http://localhost:3000`

### Push fails with authentication errors

* Ensure you've logged in for the active profile: `inkeep login`
* Local profiles with `credential: none` don't require login — the local server accepts unauthenticated requests
* Check which profile is active: `inkeep profile current`

## Authentication Issues (Local Development)

If auto-login isn't working when you open `http://localhost:3000`:

* **You recently logged out**: After clicking "Log out", a `dev-logged-out` cookie prevents auto-login so you can test login flows. To restore auto-login, clear your cookies or restart your browser.
* **`pnpm db:auth:init` not run**: The admin user must exist in the database. Run `pnpm db:auth:init` to create it.
* **API not running**: The auto-login proxy calls `POST /api/auth/dev-session` on the API server. Make sure the API is running on `localhost:3002`.
* **Bypass secret missing from `.env`**: Ensure `INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET` is set in your `.env` file. The proxy uses this to authenticate with the API. Check the server console for `[proxy]` warnings.
* **Credentials missing from `.env`**: Ensure `INKEEP_AGENTS_MANAGE_UI_USERNAME` is set in your `.env` file. The password env var is only used by `pnpm db:auth:init` to create the admin user, not for auto-login.

## Common Configuration Issues

### General Configuration Issues

* **Missing environment variables**: Ensure all required env vars are set
* **Incorrect API endpoints**: Verify you're using the right URLs
* **Network connectivity**: Check firewall and proxy settings
* **Version mismatches**: Ensure all packages are compatible

### MCP Server Connection Issues

* **MCP not able to connect**:
  * Check that the MCP server is running and accessible
* **401 Unauthorized errors**:
  * Verify that credentials are properly configured and valid
* **Connection timeouts**:
  * Ensure network connectivity and firewall settings allow connections

### AI Provider Configuration Problems

* **AI Provider key not defined or invalid**:
  * Ensure you have one of these environment variables set: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GOOGLE_GENERATIVE_AI_API_KEY`
  * Verify the API key is valid and has sufficient credits
  * Check that the key hasn't expired or been revoked

* **GPT-5 access issues**:
  * Individual users cannot access GPT-5 as it requires organization verification
  * Use GPT-4 or other available models instead
  * Contact OpenAI support if you need GPT-5 access for your organization

### Credit and Rate Limiting Issues

* **Running out of credits**:
  * Monitor your OpenAI usage and billing
  * Set up usage alerts to prevent unexpected charges

* **Rate limiting by AI providers**:
  * Especially common with high-frequency operations like summarizers
  * Monitor your API usage patterns and adjust accordingly

### Context Fetcher Issues

* **Context fetcher timeouts**:
  * Check that external services are responding within expected timeframes

## Error Retry Behavior

When calling agents the system automatically retries certain errors using exponential backoff.

The following errors are automatically retried:

| Status Code | Meaning                          |
| ----------- | -------------------------------- |
| `429`       | Too Many Requests (rate limited) |
| `500`       | Internal Server Error            |
| `502`       | Bad Gateway                      |
| `503`       | Service Unavailable              |
| `504`       | Gateway Timeout                  |

These transient network issues are also automatically retried:

* Network connectivity failures
* Connection timeouts
* `ECONNRESET` — Connection reset by peer
* `ECONNREFUSED` — Connection refused (network level)
* `ENOTFOUND` — DNS lookup failures
* Fetch/request failures


# Inkeep API (/api-reference)

Explore the Inkeep Agents API endpoints for managing, running, and evaluating agents.



import { source } from '@/lib/source';

<Cards>
  {source
      .getPages()
      .filter((item) => item.url.startsWith('/api-reference/'))
      .map((item) => (
        <Card
          key={item.url}
          title={item.data.title}
          href={item.url}
          icon={item.data.icon}
        >
          {item.data.description}
        </Card>
      ))}
</Cards>

***


# Join & Follow (/community/inkeep-community)

Join the Inkeep community on Slack, GitHub, and social media for support, updates, and collaboration.



To get help, share ideas, and provide feedback, join our community:

<Cards>
  <Card title="Slack" icon="LuSlack" href="https://inkeep-ai.slack.com/ssb/redirect">
    Get support, share ideas, and connect with other builders.
  </Card>
</Cards>

You can also find us on:

<Cards>
  <Card title="𝕏 (Twitter)" icon="LuTwitter" href="https://x.com/inkeep">
    Updates, tips, and shout‑outs for builders.
  </Card>

  <Card title="GitHub" icon="LuGithub" href="https://github.com/inkeep/agents">
    Star the repo, open issues, and contribute.
  </Card>

  <Card title="YouTube" icon="brand/Youtube" href="https://www.youtube.com/@inkeep-ai">
    Practical demos, tutorials, and deep dives.
  </Card>

  <Card title="LinkedIn" icon="LuLinkedin" href="https://www.linkedin.com/company/inkeep/">
    Company updates, launches, and hiring news.
  </Card>
</Cards>

Feel free to tag us as `@inkeep` on 𝕏 or `@Inkeep` on LinkedIn with a video of what you're building — we like to highlight neat Agent use cases from the community where possible. Also feel free to submit a PR to our [template library](https://github.com/inkeep/agents/tree/main/agents-cookbook/template-projects).

To keep up to date with all news related to AI Agents, sign up for the Agents Newsletter:

<Cards>
  <Card title="Agents Newsletter" icon="LuMail" href="https://inkeep.com/agents-newsletter">
    Get the latest AI Agents news, tips, and updates delivered to your inbox.
  </Card>
</Cards>


# License (/community/license)

License for the Inkeep Agent Framework



The Inkeep Agent Framework is licensed under the **Elastic License 2.0** ([ELv2](https://www.elastic.co/licensing/elastic-license)) subject to **Inkeep's Supplemental Terms** ([SUPPLEMENTAL\_TERMS.md](https://github.com/inkeep/agents/blob/main/SUPPLEMENTAL_TERMS.md)). This is a [fair-code](https://faircode.io/), source-available license that allows broad usage while protecting against certain competitive uses.


# Connect Your Data with Context7 (/connect-your-data/context7)

Learn how to connect your code repositories and documentation to your agents using Context7



Context7 specializes in connecting code repositories and technical documentation to your agents. It's particularly well-suited for developers who want their agents to have access to library documentation, API specs, and code repositories.

## Supported data sources

With Context7 you can connect:

* **Code Repositories**: GitHub Repositories, GitLab, BitBucket
* **API Documentation**: OpenAPI Spec
* **Documentation Formats**: LLMs.txt
* **Websites**: Website crawling and indexing

## Getting started

### Step 1: Check if your library is already available

Context7 maintains a library of pre-indexed documentation for popular libraries and frameworks. Before creating an account, check if your library is already available:

Visit [context7.com](https://context7.com/) and browse the list of available libraries. If your library is listed, you can use it immediately without additional setup.

<Tip>
  If your library is already available in Context7's library, you can skip directly to Step 4 to get the library ID and register the MCP server.
</Tip>

### Step 2: Create an account

If your library isn't listed or you want to connect custom sources:

1. [Sign up for Context7](https://context7.com/sign-in)
2. Complete the account setup process
3. Verify your email address if required

### Step 3: Connect your data sources

1. Log in to your Context7 dashboard
2. Add your repositories, websites, or OpenAPI specs
3. Wait for Context7 to index your content
4. Verify that your content is accessible

### Step 4: Get the Context7 Library ID

To use a specific library with your agents, you'll need the Context7 Library ID. You can find this ID in the library's URL on context7.com.

**How to find the Library ID:**

1. Navigate to the library page on context7.com (e.g., `https://context7.com/supabase/supabase`)
2. Copy the path after the domain name
3. The Library ID is the path portion of the URL

**Example:**

* URL: `https://context7.com/supabase/supabase`
* Library ID: `supabase/supabase`

<Note>
  If you don't know the library ID, the Context7 MCP server can automatically match libraries based on your query using the `resolve-library-id` tool. See the "Automatic library matching" section below for details.
</Note>

### Step 5: Register the MCP server

Register the Context7 MCP server as a tool in your agent configuration:

**Using TypeScript SDK:**

```typescript
import { mcpTool, subAgent } from "@inkeep/agents-sdk";

const context7Tool = mcpTool({
  id: "context7-docs",
  name: "context7_search",
  description: "Search code documentation and library references",
  serverUrl: "https://mcp.context7.com/mcp",
});

const devAgent = subAgent({
  id: "dev-agent",
  name: "Developer Assistant",
  description: "Helps with code questions using library documentation",
  prompt: `You are a developer assistant with access to code documentation.`,
  canUse: () => [context7Tool],
});
```

**Using Visual Builder:**

1. Go to the **MCP Servers** tab in the Visual Builder
2. Click "New MCP server"
3. Enter:
   * **Name**: `Context7 Documentation`
   * **URL**: `https://mcp.context7.com/mcp`
   * **Transport Type**: `Streamable HTTP` or `SSE`
4. Save the server
5. Add it to your agent by dragging it onto your agent canvas

### Step 6: Use the Context7 MCP server in your agent

Once your Context7 MCP server is registered, you can use it with a specific library ID (from Step 4) or let it automatically match libraries based on your query.

#### Specifying a library ID

If you know which library you want to use, specify its Context7 Library ID in your agent's prompt. This allows the Context7 MCP server to skip the library-matching step and directly retrieve documentation.

**Example:**

```typescript
const devAgent = subAgent({
  id: "react-agent",
  name: "React Assistant",
  description: "Helps with React development",
  prompt: `You are a React development assistant. Use library ID "websites/inkeep" 
    when searching for documentation. Use the get_library_docs tool to find React 
    documentation and examples.`,
  canUse: () => [context7Tool],
});
```

#### Automatic library matching

If you don't specify a library ID, the Context7 MCP server will automatically match libraries based on your query. The server uses the `resolve-library-id` tool to identify the appropriate library, then uses the `get_library_docs` tool to retrieve the relevant documentation.

This approach works well when:

* You're working with multiple libraries
* The library name is mentioned in the user's query
* You want the agent to dynamically select the most relevant library


# Connect Your Data with Firecrawl (/connect-your-data/firecrawl)

Connect websites to your agents using Firecrawl



## Overview

Firecrawl is a web scraping and web crawling platform that extracts clean content from web pages and converts it to markdown or structured JSON, ready for embedding and use in RAG pipelines.

With Firecrawl you can connect your agents to:

* **Websites**: Website crawling and indexing for extracting clean content from web pages
* **Web pages**: Individual page scraping with automatic content extraction

## RAG pipeline workflow

Here's what a complete RAG pipeline looks like to connect your websites to your agents:

<Steps>
  <Step>
    **Scrape web content with Firecrawl** - Extract clean markdown from websites
  </Step>

  <Step>
    **Save the clean markdown** - Store scraped content locally
  </Step>

  <Step>
    **Index the documents in Pinecone** - Using Pinecone Assistant SDK
  </Step>

  <Step>
    **Agent queries via MCP server** - Retrieves relevant content using semantic search
  </Step>
</Steps>

## Getting started

### Prerequisites

Before we get started, make sure you have the following:

* A [Firecrawl account](https://firecrawl.dev/)
* A [Pinecone account](https://app.pinecone.io/)
* [uv](https://docs.astral.sh/uv/) installed
* A python virtual environment running

### Step 1: Set up Firecrawl and collect data

Install Firecrawl and retrieve your API key from [firecrawl.dev](https://firecrawl.dev):

```bash
uv pip install firecrawl-py python-dotenv
```

Save your API key to a `.env` file:

```bash title=".env"
FIRECRAWL_API_KEY=fc-YOUR-KEY-HERE
```

The following script uses Firecrawl to explore a website's structure, identify all available pages, and convert each page's content into markdown files.

```python
from firecrawl import Firecrawl
from dotenv import load_dotenv
from pathlib import Path

load_dotenv()
app = Firecrawl()

# Crawl to discover pages
crawl_result = app.crawl(
    "https://www.mayoclinic.org/drugs-supplements",
    limit=10,
    scrape_options={'formats': ['markdown']}
)

# Extract URLs
urls = [page.metadata.url for page in crawl_result.data
        if page.metadata and page.metadata.url]

# Batch scrape
batch_job = app.batch_scrape(urls, formats=["markdown"])

# This creates a directory for our documents and saves each scraped page as a numbered markdown file
output_dir = Path("data/documents")
output_dir.mkdir(parents=True, exist_ok=True)

for i, result in enumerate(batch_job.data):
    filename = f"doc_{i:02d}.md"
    with open(output_dir / filename, "w") as f:
        f.write(result.markdown)
```

<Tip>
  For a more in-depth tutorial on programmatically scraping with Firecrawl, follow Firecrawl's [guide](https://www.firecrawl.dev/blog/best-vector-databases-2025) under the "Building a RAG Pipeline" section.
</Tip>

### Step 2: Set up Pinecone Assistant and index your documents

We'll load those markdown files, chunk them, and store them in Pinecone. First, install the required packages:

```bash
uv pip install langchain-pinecone langchain-openai pinecone langchain langchain-text-splitters
```

Set up your Pinecone API key environment variable:

```bash title=".env"
PINECONE_API_KEY=your-key
```

In your Pinecone Assistant, create a new assistant named "drug-info-rag". The code below indexes your documents in the assistant with their embeddings.

```python
import os
from pathlib import Path
from dotenv import load_dotenv
from pinecone import Pinecone

load_dotenv()

# Initialize Pinecone
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])

# Create assistant
assistant = pc.assistant.Assistant(
    assistant_name="drug-info-rag",
)

# Upload markdown files to assistant
for md_file in Path("data/documents").glob("*.md"):
    response = assistant.upload_file(
        file_path=str(md_file.absolute()),
        timeout=None
    )
    print(f"Uploaded {md_file.name}: {response}")
```

### Step 3: Get your Pinecone Assistant MCP server URL

1. Navigate to the **Settings** tab in [Pinecone Assistant](https://app.pinecone.io/)
2. Copy the MCP URL provided

### Step 4: Register the MCP server

Register the Pinecone MCP server as a tool in your agent configuration. Replace `<your-mcp-url>` with the MCP URL you copied in Step 4.

**Using TypeScript SDK:**

You can create your [credential](/typescript-sdk/credentials/overview) using keychain, nango, or environment variables, but in this example we use environment variables.

<CodeGroup>
  ```typescript title=".env"
  PINECONE_API_KEY=<your-pinecone-api-key>
  ```

  ```typescript title="agents/documentation-agent.ts"
  import { mcpTool, subAgent, credential } from "@inkeep/agents-sdk";

  const pineconeCredential = credential({
    id: 'pinecone-credential',
    name: 'Pinecone Credential',
    type: CredentialStoreType.memory,
    credentialStoreId: 'memory-default',
    retrievalParams: {
      "key": "PINECONE_API_KEY", // where PINECONE_API_KEY is the API key for your Pinecone project
    },
  });

  const pineconeTool = mcpTool({
    id: "pinecone-documents",
    name: "pinecone_search",
    description: "Search uploaded documents and files using semantic search",
    serverUrl: "<your-mcp-url>", // From Pinecone Settings tab
  });

  const docAgent = subAgent({
    id: "doc-agent",
    name: "Documentation Assistant",
    description: "Answers questions using uploaded documents",
    prompt: `You are a documentation assistant with access to company documents and files.`,
    canUse: () => [pineconeTool],
  });
  ```
</CodeGroup>

**Using Visual Builder:**

1. **Add a Pinecone credential:**
   * Go to the **Credentials** tab in the Visual Builder
   * Click **"New credential"**
   * Select **"Bearer authentication"**
   * Enter:
     * **Name**: `Pinecone API Key` (or your preferred name)
     * **API key**: Your Pinecone API key (found in your [Pinecone dashboard](https://app.pinecone.io/))
   * Click **"Create Credential"** to save

2. **Register the MCP server:**
   * Go to the **MCP Servers** tab in the Visual Builder
   * Click **"New MCP server"**
   * Select **"Custom Server"**
   * Enter:
     * **Name**: `Pinecone Documents`
     * **URL**: Your MCP URL from Pinecone Settings tab
     * **Transport Type**: `Streamable HTTP`
     * **Credential**: Select the Pinecone credential you created
   * Click **"Create"** to save the server

3. **Add the MCP tool to your sub agent:**
   * Drag the Pinecone Documents MCP tool onto your agent canvas and connect it to the sub agent

### Step 5: Use the Pinecone Assistant MCP server in your agent

Once you have registered your MCP server as a tool and connected it to your agent, your agent can use the Pinecone Assistant tool to search and retrieve relevant content from your uploaded documents.

Ask an interesting question like, "What are the primary uses of amlodipine and atorvastatin, and how do they work in the body?"

The Pinecone tool provides a `get_context` function that retrieves relevant document snippets from your knowledge base. When your agent calls this tool, it will:

<Steps>
  <Step>
    **Search semantically**: Use vector similarity search to find the most relevant content based on the query
  </Step>

  <Step>
    **Return formatted snippets**: Each result includes:

    <ul>
      <li>
        <code>file_name</code>

        : The name of the file containing the snippet
      </li>

      <li>
        <code>pages</code>

        : The page numbers where the snippet appears (for PDFs and DOCX files)
      </li>

      <li>
        <code>content</code>

        : The actual text content of the snippet
      </li>
    </ul>
  </Step>
</Steps>

**Parameters:**

* `query` (required): The search query to retrieve context for
* `top_k` (optional): The number of context snippets to retrieve. Defaults to 15.


# Connect Your Data with Inkeep Unified Search (/connect-your-data/inkeep)

Learn how to connect your data sources to your agents using Inkeep Unified Search's comprehensive knowledge base platform



Inkeep Unified Search is part of [Inkeep's Enterprise offering](https://inkeep.com/enterprise). Connect 25+ data sources to create a unified knowledge base that your agents can access.

## Supported data sources

Inkeep Unified Search supports a wide variety of data sources:

* **Documentation**: OpenAPI Spec, Plain text
* **Collaboration**: Confluence, Notion, Slack, Discord, Discourse, Missive
* **Code Repositories**: GitHub
* **Cloud Storage**: Google Drive
* **Websites**: Website crawling and indexing
* **Video**: YouTube
* **Support Systems**:
  * Freshdesk Tickets
  * HelpScout Docs and Tickets
  * Zendesk Knowledge Base, Tickets, and Help Center
* **Project Management**: Jira

## Getting started

### Step 1: Set up your Inkeep account

If you don't have an Inkeep account yet, you can:

* [Try Inkeep on your content](https://inkeep.com/demo) - Test Inkeep with your own content
* [Schedule a call](https://inkeep.com/schedule-demo) - Get personalized setup assistance

### Step 2: Connect your data sources

1. Log in to your Inkeep dashboard
2. Navigate to the "Sources" tab
3. Add and configure your desired data sources (websites, GitHub repos, Notion pages, etc.)
4. Wait for Inkeep to index your content

### Step 3: Get your MCP server URL

Once your data sources are connected and indexed, obtain your Inkeep MCP server URL from your Inkeep dashboard:

1. Go to the **Assistants** tab
2. Click **"Create Assistant"**
3. Select **MCP** from the dropdown
4. Copy the MCP server URL

### Step 4: Register the MCP server

Register your Inkeep MCP server as a tool in your agent configuration:

**Using TypeScript SDK:**

```typescript
import { mcpTool, subAgent } from "@inkeep/agents-sdk";

const inkeepTool = mcpTool({
  id: "inkeep-knowledge-base",
  name: "knowledge_base",
  description: "Search the company knowledge base powered by Inkeep",
  serverUrl: "YOUR_INKEEP_MCP_SERVER_URL", // From your Inkeep dashboard
});

const supportAgent = subAgent({
  id: "support-agent",
  name: "Support Agent",
  description: "Answers questions using the company knowledge base",
  prompt: `You are a support agent with access to the company knowledge base.`,
  canUse: () => [inkeepTool],
});
```

**Using Visual Builder:**

1. Go to the **MCP Servers** tab in the Visual Builder
2. Click "New MCP server"
3. Select "Custom Server"
4. Enter:
   * **Name**: `Inkeep Knowledge Base`
   * **URL**: Your Inkeep MCP server URL
   * **Transport Type**: `Streamable HTTP`
5. Save the server
6. Add it to your agent by dragging it onto your agent canvas

### Step 5: Use the Inkeep MCP server in your agent

Once your have registered your MCP server as a tool and connected it to your agent, it's now ready to use! Click on the "Try it" button to test it out.


# Connecting Your Data (/connect-your-data/overview)

Learn how to connect your data sources to your agents through MCP servers



Connect your data sources to your agents through MCP servers. Support websites, GitHub repositories, documentation, knowledge bases, PDFs, and more. Your agents can search and access this content in real-time.

## How it works

1. **Set up your data source** - Configure your chosen provider and connect your data sources
2. **Get your MCP server URL** - Obtain the MCP server endpoint from your provider
3. **Register the MCP server** - Register it as a tool in the Visual Builder or TypeScript SDK
4. **Use in your agents** - Your agents can now search and access your connected data

## Choose a data provider

Select a provider that supports your data sources:

<Cards>
  <Card title="Inkeep Unified Search" icon="brand/Inkeep" href="/connect-your-data/inkeep">
    Connect 25+ data sources including websites, GitHub, Notion, Slack, Confluence, and more. Comprehensive knowledge base solution.
  </Card>

  <Card title="Pinecone" icon="brand/Pinecone" href="/connect-your-data/pinecone">
    Connect DOCX, JSON, Markdown, PDF, and Text files. Ideal for document-heavy workflows and semantic search applications.
  </Card>

  <Card title="Context7" icon="brand/Upstash" href="/connect-your-data/context7">
    Connect GitHub, GitLab, BitBucket repositories, OpenAPI specs, and websites. Great for code documentation.
  </Card>

  <Card title="Ref" icon="brand/Ref" href="/connect-your-data/ref">
    Connect GitHub repositories, PDFs, and Markdown files. Simple and focused solution for documentation.
  </Card>

  <Card title="Firecrawl" icon="brand/Firecrawl" href="/connect-your-data/firecrawl">
    Connect websites. Great for extracting LLM-friendly content from the web.
  </Card>
</Cards>

## Other options

Consider these additional options for connecting your data:

* **[Reducto](https://reducto.ai/)** - Self-serve document processing with a free tier. Upload documents and access them via APIs or SDKs. Wrap their APIs in your own MCP server or invoke them directly using [function tools](/typescript-sdk/tools/function-tools).

* **[Unstructured](https://unstructured.io/)** - Document processing platform with a free tier. Upload documents and access them via APIs or SDKs. Wrap their APIs in your own MCP server or invoke them directly using [function tools](/typescript-sdk/tools/function-tools).

* **[Milvus](https://milvus.io/)** - Open-source vector database with self-hosting options. See their [MCP integration guide](https://milvus.io/docs/milvus_and_mcp.md) for setup instructions. Must manage deployment and hosting for MCP server.

* **[SingleStore](https://singlestore.com/)** - Relational database with a managed MCP server that converts natural language queries into SQL. Learn more in their [MCP server documentation](https://docs.singlestore.com/cloud/ai-services/singlestore-mcp-server/).


# Connect Your Data with Pinecone (/connect-your-data/pinecone)

Connect documents to your agents using Pinecone Assistant's MCP server



Pinecone is a vector database, and Pinecone Assistant helps you build production-grade chat and agent applications. Connect your documents and files to your agents using Pinecone Assistant's MCP server for semantic search and retrieval.

## Supported data sources

With Pinecone Assistant you can connect:

* **Documents**: DOCX (.docx), PDF (.pdf), Text (.txt)
* **Structured Data**: JSON (.json)
* **Documentation**: Markdown (.md)

## Getting started

### Step 1: Create a Pinecone account

[Sign up for Pinecone](https://app.pinecone.io/)

### Step 2: Create an Assistant

1. Log in to your [Pinecone dashboard](https://app.pinecone.io/)
2. Navigate to the **Assistant** tab
3. Click **"Create an Assistant"**
4. Give your assistant a name

### Step 3: Upload your files

1. In your assistant, navigate to the **Files** tab (located in the top right corner)
2. Upload your documents (DOCX, JSON, Markdown, PDF, or Text files)
3. Wait for Pinecone to process and index your content

<Tip>
  Test your assistant in the Assistant playground. Try uploading an Apple 10-K PDF file from [Apple's investor relations page](https://investor.apple.com/sec-filings/default.aspx) and ask it to summarize the document.
</Tip>

### Step 4: Get your MCP server URL

1. Navigate to the **Settings** tab in your assistant
2. Copy the MCP URL provided

### Step 5: Register the MCP server

Register the Pinecone MCP server as a tool in your agent configuration. Replace `<your-mcp-url>` with the MCP URL you copied in Step 4.

**Using TypeScript SDK:**

You can create your [credential](/typescript-sdk/credentials/overview) using keychain, nango, or environment variables, but in this example we use environment variables.

<CodeGroup>
  ```typescript title=".env"
  PINECONE_API_KEY=<your-pinecone-api-key>
  ```

  ```typescript title="agents/documentation-agent.ts"
  import { mcpTool, subAgent, credential } from "@inkeep/agents-sdk";

  const pineconeCredential = credential({
    id: 'pinecone-credential',
    name: 'Pinecone Credential',
    type: CredentialStoreType.memory,
    credentialStoreId: 'memory-default',
    retrievalParams: {
      "key": "PINECONE_API_KEY", // where PINECONE_API_KEY is the API key for your Pinecone project
    },
  });

  const pineconeTool = mcpTool({
    id: "pinecone-documents",
    name: "pinecone_search",
    description: "Search uploaded documents and files using semantic search",
    serverUrl: "<your-mcp-url>", // From Pinecone Settings tab
  });

  const docAgent = subAgent({
    id: "doc-agent",
    name: "Documentation Assistant",
    description: "Answers questions using uploaded documents",
    prompt: `You are a documentation assistant with access to company documents and files.`,
    canUse: () => [pineconeTool],
  });
  ```
</CodeGroup>

**Using Visual Builder:**

1. **Add a Pinecone credential:**
   * Go to the **Credentials** tab in the Visual Builder
   * Click **"New credential"**
   * Select **"Bearer authentication"**
   * Enter:
     * **Name**: `Pinecone API Key` (or your preferred name)
     * **API key**: Your Pinecone API key (found in your [Pinecone dashboard](https://app.pinecone.io/))
   * Click **"Create Credential"** to save

2. **Register the MCP server:**
   * Go to the **MCP Servers** tab in the Visual Builder
   * Click **"New MCP server"**
   * Select **"Custom Server"**
   * Enter:
     * **Name**: `Pinecone Documents`
     * **URL**: Your MCP URL from Pinecone Settings tab
     * **Transport Type**: `Streamable HTTP`
     * **Credential**: Select the Pinecone credential you created
   * Click **"Create"** to save the server

3. **Add the MCP tool to your sub agent:**
   * Drag the Pinecone Documents MCP tool onto your agent canvas and connect it to the sub agent

### Step 6: Use the Pinecone Assistant MCP server in your agent

Once you have registered your MCP server as a tool and connected it to your agent, your agent can use the Pinecone Assistant tool to search and retrieve relevant content from your uploaded documents.

The Pinecone tool provides a `get_context` function that retrieves relevant document snippets from your knowledge base. When your agent calls this tool, it will:

<Steps>
  <Step>
    **Search semantically**: Use vector similarity search to find the most relevant content based on the query
  </Step>

  <Step>
    **Return formatted snippets**: Each result includes:

    <ul>
      <li>
        <code>file_name</code>

        : The name of the file containing the snippet
      </li>

      <li>
        <code>pages</code>

        : The page numbers where the snippet appears (for PDFs and DOCX files)
      </li>

      <li>
        <code>content</code>

        : The actual text content of the snippet
      </li>
    </ul>
  </Step>
</Steps>

**Parameters:**

* `query` (required): The search query to retrieve context for
* `top_k` (optional): The number of context snippets to retrieve. Defaults to 15.


# Connect Your Data with Ref (/connect-your-data/ref)

Learn how to connect your documentation and code repositories to your agents using Ref



Ref provides a simple and focused solution for connecting your documentation and code repositories to your agents. With support for GitHub repositories, PDFs, and Markdown files, Ref is perfect for teams that need straightforward documentation access.

## Supported data sources

With Ref you can connect:

* **Code Repositories**: GitHub Repositories
* **Documents**: PDF files
* **Documentation**: Markdown files

## Getting started

### Step 1: Create an account

1. [Sign up for Ref](https://ref.tools/login)
2. Complete the account registration process

### Step 2: Upload your resources

1. Log in to your Ref dashboard
2. Navigate to the [Resources page](https://ref.tools/resources)
3. Upload your PDFs, Markdown files, or connect your GitHub repositories
4. Wait for Ref to process and index your content

### Step 3: Get your MCP server URL

1. Navigate to the [Install MCP page](https://ref.tools/install)
2. Copy the MCP server URL (it will start with `https://api.ref.tools/mcp?apiKey=ref-`)

### Step 4: Register the MCP server

Register the Ref MCP server as a tool in your agent configuration. Replace `<your-api-key>` with your actual API key from Step 3.

**Using TypeScript SDK:**

```typescript
import { mcpTool, subAgent } from "@inkeep/agents-sdk";

const refTool = mcpTool({
  id: "ref-documentation",
  name: "ref_search",
  description: "Search uploaded documentation and code repositories",
  serverUrl: "https://api.ref.tools/mcp?apiKey=ref-<your-api-key>",
});

const docAgent = subAgent({
  id: "doc-agent",
  name: "Documentation Assistant",
  description: "Answers questions using uploaded documentation",
  prompt: `You are a documentation assistant with access to company documentation`,
  canUse: () => [refTool],
});
```

**Using Visual Builder:**

1. Go to the **MCP Servers** tab in the Visual Builder
2. Click "New MCP server"
3. Select "Custom Server"
4. Enter:
   * **Name**: `Ref Documentation`
   * **URL**: `https://api.ref.tools/mcp?apiKey=ref-<your-api-key>` (replace with your API key)
   * **Transport Type**: `Streamable HTTP`
5. Save the server
6. Add it to your agent by dragging it onto your agent canvas

### Step 5: Use the Ref MCP server in your agent

Once your have registered your MCP server as a tool and connected it to your agent, it's now ready to use! Click on the "Try it" button to test it out.


# Deploy to Vercel (/deployment/vercel)

Deploy the Inkeep Agent Framework to Vercel



## Deploy to Vercel

### Step 1: Create a GitHub repository for your project

If you do not have an Inkeep project already, [follow these steps](/get-started/quick-start) to create one. Then push your project to a repository on GitHub.

### Step 2: Create a Postgres Database

Create a Postgres database on the [**Vercel Marketplace**](https://vercel.com/marketplace/neon) or directly at [**Neon**](https://neon.tech/).

### Step 3: Create a Doltgres Database

Create a Doltgres database at [**DoltHub**](https://hosted.doltdb.com).

### Step 4: Configure Database Connection

Set your database connection string as an environment variable:

```dotenv
# Doltgres Database
INKEEP_AGENTS_MANAGE_DATABASE_URL=
# Postgres Database
INKEEP_AGENTS_RUN_DATABASE_URL=
```

### Step 5: Create a Vercel account

Sign up for a Vercel account [here](https://vercel.com/signup).

### Step 6: Create a Vercel project for Manage API

<img src="/images/vercel-new-project-manage-api-hono.png" alt="Vercel New Project - Manage API" style={{ borderRadius: '8px' }} />

<Note>
  The Framework Preset should be "Hono" and the Root Directory should be
  `apps/manage-api`.
</Note>

Required environment variables for Manage API:

```dotenv
ENVIRONMENT=production

INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET=

# Doltgres Database
INKEEP_AGENTS_MANAGE_DATABASE_URL=
# Postgres Database
INKEEP_AGENTS_RUN_DATABASE_URL=

NANGO_SECRET_KEY=
NANGO_SERVER_URL=https://api.nango.dev
```

| Environment Variable                     | Value                                                                                                                                            |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ENVIRONMENT`                            | `production`                                                                                                                                     |
| `INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET` | Run `openssl rand -hex 32` in your terminal to generate this value. Save this value for `INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET` in Step 7.      |
| `INKEEP_AGENTS_MANAGE_DATABASE_URL`      | Doltgres connection string from Step 3 (e.g., `postgresql://user:password@host:5432/database`)                                                   |
| `INKEEP_AGENTS_RUN_DATABASE_URL`         | Postgres connection string from Step 4 (e.g., `postgresql://user:password@host:5433/database`)                                                   |
| `NANGO_SECRET_KEY`                       | Nango secret key from your [Nango Cloud account](/typescript-sdk/credentials/nango). Note: Local Nango setup won't work with Vercel deployments. |
| `NANGO_SERVER_URL`                       | `https://api.nango.dev`                                                                                                                          |

### Step 7: Create a Vercel project for Run API

<img src="/images/vercel-new-project-run-api-hono.png" alt="Vercel New Project - Run API" style={{ borderRadius: '8px' }} />

<Note>
  The Framework Preset should be "Hono" and the Root Directory should be
  `apps/run-api`.
</Note>

Required environment variables for Run API:

```dotenv
ENVIRONMENT=production

ANTHROPIC_API_KEY=
OPENAI_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY=
AI_GATEWAY_API_KEY=

INKEEP_AGENTS_RUN_API_BYPASS_SECRET=

# Postgres Database
INKEEP_AGENTS_RUN_DATABASE_URL=

OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://ingest.us.signoz.cloud:443/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS=signoz-ingestion-key=<your-ingestion-key>

NANGO_SECRET_KEY=
NANGO_SERVER_URL=https://api.nango.dev

INKEEP_AGENTS_JWT_SIGNING_SECRET=

# Optional: set to 'false' to disable Inkeep-attached Anthropic prompt caching
# INKEEP_PROMPT_CACHING_ENABLED=false
```

| Environment Variable                  | Value                                                                                                                                                                                                                                      |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ENVIRONMENT`                         | `production`                                                                                                                                                                                                                               |
| `ANTHROPIC_API_KEY`                   | Your Anthropic API key                                                                                                                                                                                                                     |
| `OPENAI_API_KEY`                      | Your OpenAI API key                                                                                                                                                                                                                        |
| `GOOGLE_GENERATIVE_AI_API_KEY`        | Your Google Gemini API key                                                                                                                                                                                                                 |
| `INKEEP_AGENTS_RUN_API_BYPASS_SECRET` | Run `openssl rand -hex 32` in your terminal to generate this value. Save this value for `INKEEP_AGENTS_RUN_API_BYPASS_SECRET` in Step 7.                                                                                                   |
| `AI_GATEWAY_API_KEY`                  | (Optional) Your Vercel AI Gateway API key. When set, Anthropic, OpenAI, and Google models are automatically routed through the gateway for per-request cost tracking. See [Model Configuration](/typescript-sdk/models) for details.       |
| `INKEEP_AGENTS_RUN_DATABASE_URL`      | Postgres connection string from Step 3 (e.g., `postgresql://user:password@host:5432/database`)                                                                                                                                             |
| `NANGO_SECRET_KEY`                    | Nango secret key from your [Nango Cloud account](/typescript-sdk/credentials/nango). Note: Local Nango setup won't work with Vercel deployments.                                                                                           |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`  | `https://ingest.us.signoz.cloud:443/v1/traces`                                                                                                                                                                                             |
| `OTEL_EXPORTER_OTLP_TRACES_HEADERS`   | `signoz-ingestion-key=<your-ingestion-key>`. Use the instructions from [SigNoz Cloud Setup](/get-started/traces#option-1-signoz-cloud-setup) to configure your ingestion key. Note: Local SigNoz setup won't work with Vercel deployments. |
| `NANGO_SERVER_URL`                    | `https://api.nango.dev`                                                                                                                                                                                                                    |
| `INKEEP_AGENTS_JWT_SIGNING_SECRET`    | Run `openssl rand -hex 32` in your terminal to generate this value. Save this value for `INKEEP_AGENTS_JWT_SIGNING_SECRET` in Step 7.                                                                                                      |
| `INKEEP_PROMPT_CACHING_ENABLED`       | (Optional) Set to `false` to disable Inkeep-attached Anthropic prompt caching globally. Defaults to `true`. See [Prompt Caching](/guides/observability/prompt-caching) for details.                                                        |

### Step 8: Create a Vercel project for Manage UI

<img src="/images/vercel-new-project-manage-ui-nextjs.png" alt="Vercel New Project - Manage UI" style={{ borderRadius: '8px' }} />

<Note>
  The Framework Preset should be "Next.js" and the Root Directory should be
  `apps/manage-ui`.
</Note>

Required environment variables for Manage UI:

```dotenv
ENVIRONMENT=production

PUBLIC_INKEEP_AGENTS_RUN_API_URL=
PUBLIC_INKEEP_AGENTS_RUN_API_BYPASS_SECRET=

PUBLIC_INKEEP_AGENTS_MANAGE_API_URL=
INKEEP_AGENTS_MANAGE_API_URL=
INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET=

PUBLIC_SIGNOZ_URL=https://<your-organization>.signoz.cloud
SIGNOZ_API_KEY=

PUBLIC_NANGO_SERVER_URL=https://api.nango.dev
PUBLIC_NANGO_CONNECT_BASE_URL=https://connect.nango.dev
NANGO_SECRET_KEY=
```

| Environment Variable                         | Value                                                                                                                                                                                          |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENVIRONMENT`                                | `production`                                                                                                                                                                                   |
| `PUBLIC_INKEEP_AGENTS_RUN_API_URL`           | Your Vercel deployment URL for Run API                                                                                                                                                         |
| `PUBLIC_INKEEP_AGENTS_RUN_API_BYPASS_SECRET` | Your generated Run API bypass secret from Step 6                                                                                                                                               |
| `PUBLIC_INKEEP_AGENTS_MANAGE_API_URL`        | Your Vercel deployment URL for Manage API (skip if same as `INKEEP_AGENTS_MANAGE_API_URL`)                                                                                                     |
| `INKEEP_AGENTS_MANAGE_API_URL`               | Your Vercel deployment URL for Manage API                                                                                                                                                      |
| `INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET`     | Your generated Manage API bypass secret from Step 5                                                                                                                                            |
| `PUBLIC_SIGNOZ_URL`                          | Use the instructions from [SigNoz Cloud Setup](/get-started/traces#option-1-signoz-cloud-setup) to configure your SigNoz URL. Note: Local SigNoz setup won't work with Vercel deployments.     |
| `SIGNOZ_API_KEY`                             | Use the instructions from [SigNoz Cloud Setup](/get-started/traces#option-1-signoz-cloud-setup) to configure your SigNoz API key. Note: Local SigNoz setup won't work with Vercel deployments. |
| `NANGO_SECRET_KEY`                           | Nango secret key from your [Nango Cloud account](/typescript-sdk/credentials/nango). Note: Local Nango setup won't work with Vercel deployments.                                               |
| `PUBLIC_NANGO_SERVER_URL`                    | `https://api.nango.dev`                                                                                                                                                                        |
| `PUBLIC_NANGO_CONNECT_BASE_URL`              | `https://connect.nango.dev`                                                                                                                                                                    |

### Step 9: Enable Vercel Authentication

To prevent anyone from being able to access the UI, we recommend enabling Vercel authentication for all deployments: **Settings > Deployment Protection > Vercel Authentication > All Deployments**.

### Step 10: Create a Vercel project for your MCP server (optional)

<img src="/images/vercel-new-project-mcp.png" alt="Vercel New Project - MCP Server" style={{ borderRadius: '8px' }} />

<Note>
  The Framework Preset should be "Next.js" and the Root Directory should be
  `apps/mcp`.
</Note>

For more information on how to add MCP servers to your project, see [Create MCP Servers](/typescript-sdk/cli-reference#inkeep-add).

## Push your Agent

### Step 1: Configure your root .env file

```dotenv
INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET=
INKEEP_AGENTS_RUN_API_BYPASS_SECRET=
```

### Step 2: Create a cloud configuration file

Create a new configuration file named `inkeep-cloud.config.ts` in your project's `src` directory, alongside your existing configuration file.

```typescript
import { defineConfig } from "@inkeep/agents-cli/config";

const config = defineConfig({
  tenantId: "default",
  agentsManageApi: {
    url: "https://<your-manage-api-url>",
    apiKey: process.env.INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET,
  },
  agentsRunApi: {
    url: "https://<your-run-api-url>",
    apiKey: process.env.INKEEP_AGENTS_RUN_API_BYPASS_SECRET,
  },
});

export default config;
```

### Step 3: Push your Agent

```bash
cd /src/<your-project-directory>
inkeep push --config ../inkeep-cloud.config.ts
```

## Pull your Agent

```bash
cd /src
inkeep pull --config inkeep-cloud.config.ts
```

## Function Tools with Vercel Sandbox

When deploying to serverless environments like Vercel, you can configure [function tools](/typescript-sdk/tools/function-tools) to execute in [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) MicroVMs instead of your Agent's runtime service. This is **required** for serverless platforms since child process spawning is restricted.

### Why Use Vercel Sandbox?

**When to use each provider:**

* **Native** - Use for traditional cloud deployments (VMs, Docker, Kubernetes), self-hosted servers, or local development
* **Vercel Sandbox** - Required for serverless platforms (Vercel, AWS Lambda, etc.) or if you'd like to isolate tool executions

### Setting Up Vercel Sandbox

#### Step 1: Get Vercel Credentials

You'll need three credentials from your Vercel account:

1. **Vercel Token** - Create an access token at [vercel.com/account/tokens](https://vercel.com/account/tokens)
2. **Team ID** - Find in your team settings at [vercel.com/teams](https://vercel.com/teams)
3. **Project ID** - Find in your Vercel project settings

#### Step 2: Configure Sandbox in Your Application

Update your Run API to use Vercel Sandbox.

In the `apps/run-api/src` folder, create a `sandbox.ts` file:

```typescript sandbox.ts
const isProduction = process.env.ENVIRONMENT === "production";

export const sandboxConfig = isProduction
  ? {
      provider: "vercel",
      runtime: "node22", // or 'typescript'
      timeout: 60000, // 60 second timeout
      vcpus: 4, // Allocate 4 vCPUs
      teamId: process.env.SANDBOX_VERCEL_TEAM_ID!,
      projectId: process.env.SANDBOX_VERCEL_PROJECT_ID!,
      token: process.env.SANDBOX_VERCEL_TOKEN!,
    }
  : {
      provider: "native",
      runtime: "node22",
      timeout: 30000,
      vcpus: 2,
    };
```

Import it into your `index.ts` file:

```typescript index.ts
import { sandboxConfig } from "./sandbox.ts";
// ...
const app: Hono = createExecutionApp({
  // ...
  sandboxConfig, // NEW
});
```

#### Step 3: Add Environment Variables to Run API

Add these [environment variables in your Vercel project](https://vercel.com/docs/environment-variables/managing-environment-variables#declare-an-environment-variable) to your **Run API** app:

```dotenv
SANDBOX_VERCEL_TOKEN=your_vercel_access_token
SANDBOX_VERCEL_TEAM_ID=team_xxxxxxxxxx
SANDBOX_VERCEL_PROJECT_ID=prj_xxxxxxxxxx
```

<Accordions>
  <Accordion title="Troubleshooting Functions">
    <strong>
      "Failed to refresh OIDC token" error:
    </strong>

    <ul>
      <li>
        This occurs when you're not in a Vercel environment or you don't provide a Vercel access token
      </li>

      <li>
        Solution: Use a Vercel access token from
        <a href="https://vercel.com/account/tokens" target="_blank" rel="noopener noreferrer">vercel.com/account/tokens</a>
      </li>
    </ul>

    <strong>
      Function execution timeouts:
    </strong>

    <ul>
      <li>
        Increase the 

        <code>timeout</code>

         value in sandbox configuration
      </li>

      <li>
        Consider allocating more 

        <code>vcpus</code>

         for resource-intensive functions
      </li>

      <li>
        Check Vercel Sandbox limits for your plan
      </li>
    </ul>

    <strong>
      Dependency installation failures:
    </strong>

    <ul>
      <li>
        Ensure dependencies are compatible with Node.js 22
      </li>

      <li>
        Check that package versions are specified correctly
      </li>

      <li>
        Verify network access to npm registry
      </li>
    </ul>

    <strong>
      High costs:
    </strong>

    <ul>
      <li>
        Reduce 

        <code>vcpus</code>

         allocation if functions don't need maximum resources
      </li>

      <li>
        Optimize function code to execute faster
      </li>

      <li>
        Consider caching results when possible
      </li>
    </ul>
  </Accordion>

  <Accordion title="Best Practices">
    <ol>
      <li>
        <strong>Use environment variables</strong>

         – Never hardcode credentials
      </li>

      <li>
        <strong>Start with fewer vCPUs</strong>

         – Scale up only if needed
      </li>

      <li>
        <strong>Set reasonable timeouts</strong>

         – Prevent runaway executions
      </li>

      <li>
        <strong>Monitor usage</strong>

         – Track sandbox execution metrics in Vercel dashboard
      </li>

      <li>
        <strong>Test thoroughly</strong>

         – Verify functions work in sandbox environment before deploying
      </li>

      <li>
        <strong>Choose the right provider</strong>

         – Use native for VMs/Docker/K8s, Vercel Sandbox for serverless only
      </li>
    </ol>

    <p>
      For more information on function tools, see:
    </p>

    <ul>
      <li>
        <a href="/typescript-sdk/tools/function-tools">
          Function Tools (TypeScript SDK)
        </a>
      </li>

      <li>
        <a href="/visual-builder/tools/function-tools">
          Function Tools (Visual Builder)
        </a>
      </li>

      <li>
        <a href="https://vercel.com/docs/vercel-sandbox" target="_blank" rel="noopener noreferrer">
          Vercel Sandbox Documentation
        </a>
      </li>
    </ul>
  </Accordion>
</Accordions>

## Deployment Protection

The Inkeep Agent Framework provides health endpoints for Kubernetes-style probes and optional integration with Vercel Checks for automated deployment protection.

### Health Endpoints

Two health endpoints are available for monitoring deployment readiness:

#### `/health` - Liveness Probe

A lightweight endpoint that returns immediately to indicate the service is running.

* **Response**: HTTP 204 (No Content)
* **Use case**: Kubernetes liveness probes, load balancer health checks
* **Latency**: Sub-millisecond (no external calls)

```bash
curl -I https://your-api.vercel.app/health
# HTTP/2 204
```

#### `/ready` - Readiness Probe

A comprehensive endpoint that verifies database connectivity before serving traffic.

* **Response (healthy)**: HTTP 200 with JSON status
* **Response (unhealthy)**: HTTP 503 with RFC 7807 Problem Details
* **Use case**: Kubernetes readiness probes, deployment verification

```bash
# Healthy response
curl https://your-api.vercel.app/ready
{
  "status": "ok",
  "manageDb": true,
  "runDb": true
}

# Unhealthy response (503 Service Unavailable)
{
  "type": "about:blank",
  "title": "Service Unavailable",
  "status": 503,
  "detail": "One or more health checks failed",
  "checks": {
    "manageDb": false,
    "runDb": true
  }
}
```

### Vercel Deployment Checks with GitHub Actions

Enable automated deployment protection using [Vercel Deployment Checks](https://vercel.com/docs/deployment-checks) combined with a GitHub Action. This approach blocks production deployments from being promoted until your health checks pass.

#### How It Works

1. **Deployment Created**: Vercel creates a production deployment but doesn't promote it yet
2. **GitHub Action Runs**: A workflow triggers when the deployment is ready and hits the `/ready` endpoint
3. **Status Reported**: The GitHub Action reports success/failure back to GitHub
4. **Vercel Reads Status**: Vercel reads the GitHub commit status and promotes the deployment only if checks pass

#### Step 1: Add the GitHub Action Workflow

The Inkeep Agent Framework includes a pre-configured workflow at `.github/workflows/deployment-health-check.yml`:

```yaml
name: Deployment Health Check

on:
  deployment_status:

jobs:
  health-check:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - name: Wait for cold start
        run: sleep 10

      - name: Check /health endpoint (liveness)
        run: |
          curl -f -s -o /dev/null -w "%{http_code}" \
            "${{ github.event.deployment_status.target_url }}/health" \
            --retry 3 --retry-delay 5

      - name: Check /ready endpoint (readiness)
        run: |
          response=$(curl -f -s "${{ github.event.deployment_status.target_url }}/ready" \
            --retry 3 --retry-delay 5)
          echo "Response: $response"
          echo "$response" | jq -e '.status == "ok"'
```

#### Step 2: Enable Deployment Checks in Vercel

1. Ensure your project is connected to GitHub using [Vercel for GitHub](https://vercel.com/docs/git/vercel-for-github)
2. Go to **Project Settings > Deployment Checks**
3. Click **Add Checks** and select the `health-check` job from your GitHub Actions
4. Production deployments will now wait for the health check to pass before being promoted

#### Step 3: Test the Integration

1. Push a change to your default branch
2. Vercel creates a production deployment
3. The GitHub Action runs and checks the `/ready` endpoint
4. Once the check passes, Vercel promotes the deployment to your production domains

<Accordions>
  <Accordion title="Troubleshooting Deployment Checks">
    <strong>
      GitHub Action not running:
    </strong>

    <ul>
      <li>
        Ensure the workflow file exists at 

        <code>.github/workflows/deployment-health-check.yml</code>
      </li>

      <li>
        Verify the workflow is enabled in your repository's Actions settings
      </li>
    </ul>

    <strong>
      Check not appearing in Vercel:
    </strong>

    <ul>
      <li>
        Verify your project is connected to GitHub via Vercel for GitHub
      </li>

      <li>
        Ensure the check is selected in Project Settings > Deployment Checks
      </li>
    </ul>

    <strong>
      Health check fails but service is healthy:
    </strong>

    <ul>
      <li>
        The 

        <code>/ready</code>

         endpoint checks database connectivity
      </li>

      <li>
        Ensure database connection strings are correct in the deployment
      </li>

      <li>
        Check database accessibility from the Vercel deployment region
      </li>

      <li>
        Increase the cold start wait time if needed
      </li>
    </ul>

    <strong>
      Bypassing checks:
    </strong>

    <ul>
      <li>
        Use 

        <strong>Force Promote</strong>

         from the deployment details page in Vercel
      </li>
    </ul>
  </Accordion>
</Accordions>


# Set Up Inkeep for Your IDE (/get-started/ai-coding-setup-for-ide)

Install Skills and MCP to help AI coding assistants build Inkeep agents.



Set up your IDE so AI coding assistants can help you build Inkeep agents.

## Install Inkeep skills

Install [Inkeep skills](https://github.com/inkeep/skills) so AI assistants know how to use the SDK:

```bash
npx skills add inkeep/skills
```

## Install Inkeep MCP

If you didn't opt-in during `npx @inkeep/create-agents`, add the Inkeep MCP server to your IDE:

### Cursor

Click the button below to add the Inkeep MCP server to Cursor.

<div style={{ display: "flex", flexDirection: "row", alignItems: "center", gap: "16px", justifyContent: "center" }}>
  <a href="cursor://anysphere.cursor-deeplink/mcp/install?name=inkeep-agents&config=eyJ1cmwiOiJodHRwczovL2FnZW50cy5pbmtlZXAuY29tL21jcCJ9">
    <img src="https://cursor.com/deeplink/mcp-install-dark.png" alt="Add to Cursor" style={{ height: "36px", verticalAlign: "middle", marginTop: "10px", marginBottom: "10px" }} />
  </a>
</div>

### VS Code

Click the button below to add the Inkeep MCP server to VS Code.

<div style={{ display: "flex", flexDirection: "row", alignItems: "center", gap: "16px", justifyContent: "center" }}>
  <a href="vscode:mcp/install?%7B%22name%22%3A%22inkeep-agents-mcp%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Fagents.inkeep.com%2Fmcp%22%7D">
    <img src="https://img.shields.io/badge/VS_Code-Install_Inkeep_MCP-0098FF?style=flat-square&logo=visualstudiocode&logoColor=ffffff" alt="Install in VS Code" style={{ height: "28px", verticalAlign: "middle", marginTop: "10px", marginBottom: "10px" }} />
  </a>
</div>

### Claude Code

To add to **Claude Code**, run this in your terminal:

```bash
claude mcp add --transport http inkeep-agents https://agents.inkeep.com/mcp --scope project
```

### Other MCP clients

Manually add `https://agents.inkeep.com/mcp` as an MCP Server to any MCP client.


# Push / Pull (/get-started/push-pull)

Push and pull your agents to and from the Visual Builder



## Push code to visual

With Inkeep, you can define your agents in code, push them to the Visual Builder, and continue developing with the intuitive drag-and-drop interface. You can switch back to code any time.

Let's walk through the process.

<Video src="https://www.youtube.com/watch?v=2-j4LuabeHQ" title="Using Inkeep push" />

### Step 1: Install the Inkeep CLI

```bash
pnpm install -g @inkeep/agents-cli
```

### Step 2: Download a template project

Navigate to the `src/projects` directory.

```bash
cd src/projects
```

Add the docs assistant agent using `inkeep add`.

```bash
inkeep add --project docs-assistant
```

Find the downloaded code in `src/projects/docs-assistant`.

<Tip>
  `inkeep add` imports a template project from our [cookbook library](https://github.com/inkeep/agents/tree/main/agents-cookbook/template-projects).
</Tip>

### Step 3: Push code to visual

Navigate to your docs assistant project.

```bash
cd docs-assistant
```

Use `inkeep push` to push the code to the Visual Builder.

```bash
inkeep push
```

<Tip>
  To validate your project without pushing, use the `--json` flag: `inkeep push --json`
</Tip>

### Step 4: Chat with your agent

Refresh [http://localhost:3000](http://localhost:3000), switch to the **Docs Assistant** project (in the bottom left).

Under **Agents**, click on the Docs Assistant agent and press **Try it**. Ask a question about Inkeep.

<Image src="/gifs/docs-assistant.gif" alt="Chat with your agent" />

## Run `inkeep pull`

Make some changes, like changing a prompt, and let's walk through `inkeep pull`.

<>
  ### Prerequisite

  The `inkeep pull` command in-part leverages AI to sync your TypeScript files to the state of your Visual Builder, so **at least one** of the below environment variables to be defined:

  ```txt .env
  # Choose one:
  ANTHROPIC_API_KEY=your_api_key_here
  # or
  OPENAI_API_KEY=your_api_key_here
  # or
  GOOGLE_API_KEY=your_api_key_here
  ```

  The CLI prioritizes Anthropic → OpenAI → Google.

  Here are the models used:

  | Provider  | Model(s)                              | Where to Get API Key                                |
  | --------- | ------------------------------------- | --------------------------------------------------- |
  | Anthropic | Claude Sonnet 4.5 (extended thinking) | [Anthropic Console](https://console.anthropic.com/) |
  | OpenAI    | GPT-5.1                               | [OpenAI Platform](https://platform.openai.com/)     |
  | Google    | Gemini 2.5 Flash                      | [Google AI Studio](https://ai.google.dev/)          |
</>

### Step 1: Make an edit visually

Make an edit to the docs assistant agent in the UI, such as changing the prompt of the agent.

### Step 2: Pull code from visual

Navigate to your docs assistant project.

```bash
cd src/projects/docs-assistant
```

Use `inkeep pull` to pull the code from the Visual Builder to your local project.

```bash
inkeep pull
```

### Step 3: Verify the code was pulled

Check the `src/projects/docs-assistant/agents/docs-assistant.ts` file for the updated prompt.

```bash
cat src/projects/docs-assistant/agents/docs-assistant.ts
```

## Next steps

Next, we recommend setting up observability to see live traces of your agent. See [Traces](/get-started/traces) to get started.

<Cards>
  <Card title="Traces" icon="LuActivity" href="/get-started/traces">
    Set up SigNoz to enable live debugging capabilities for your agents
  </Card>
</Cards>


# Quick Start (/get-started/quick-start)

Get started with Inkeep Agents in <5min



<>
  ## Launch your first agent

  ### Prerequisites

  Before getting started, ensure you have the following installed on your system:

  * [Node.js](https://nodejs.org/en/download/) version 22 or higher
  * [Docker](https://docs.docker.com/get-docker/)
  * [pnpm](https://pnpm.io/installation) version 10 or higher

  You can verify by running:

  ```bash
  node --version
  pnpm --version
  docker --version
  ```

  ### Step 1: Create a new agents project

  Run the quickstart script on a target folder:

  ```bash
  npx @inkeep/create-agents my-agents
  ```

  Navigate to the folder

  ```bash
  cd my-agents
  ```

  Open the folder using your coding editor. To open with Cursor, you can run `cursor .`

  ### Step 2: Run the setup script

  Ensure Docker Desktop (or Docker daemon) is running before running the setup script.

  ```bash
  pnpm setup-dev
  ```

  ### Step 3: Launch the dev environment

  ```bash
  pnpm dev
  ```

  The Visual Builder will auto-open at [http://localhost:3000](http://localhost:3000).

  ### Step 4: Login to the Visual Builder

  Use the credentials from your root `.env` file (`INKEEP_AGENTS_MANAGE_UI_USERNAME` and `INKEEP_AGENTS_MANAGE_UI_PASSWORD`) to log in when the Visual Builder opens.

  ### Step 5: Chat with your agent

  Navigate to the **Activities Planner** agent at [http://localhost:3000](http://localhost:3000) and ask about fun activities at a location of your choice:

    <img alt="Chat with your agent" src="https://docs.inkeep.com/gifs/activities-planner.gif" width="2080" height="1080" />

  ### Next steps

  * Set up [AI coding for Inkeep](https://docs.inkeep.com/get-started/ai-coding-setup-for-ide) with skills and MCP.
  * Learn about [inkeep push / pull](https://docs.inkeep.com/get-started/push-pull) so you can go from `SDK -> Visual Builder` and `Visual Builder -> SDK`.
  * Follow our [meeting prep agent tutorial](https://docs.inkeep.com/tutorials/agents/meeting-prep-assistant) to create an agent using the Visual Builder.
  * Follow our [fact finder agent tutorial](https://docs.inkeep.com/tutorials/agents/fact-finder) to create an agent using the TypeScript SDK.
</>


# Live Debugger, Traces, and OTEL Telemetry (/get-started/traces)

Set up SigNoz to enable full observability with traces and live debugging capabilities for your agents.



## Overview

The Inkeep Agent Framework provides powerful **traces** and **live debugging** capabilities powered by SigNoz. Setting up SigNoz gives you:

* **Real-time trace visualization** - See exactly how your agents execute step-by-step
* **Live debugging** - Debug agent conversations as they happen
* **Export traces as JSON** - Copy complete traces for offline analysis and debugging
* **Full observability** - Complete OpenTelemetry instrumentation for monitoring
* **Performance insights** - Identify bottlenecks and optimize agent performance

<Image src="/images/live-traces.png" alt="Live traces interface showing real-time agent execution" />

## Setup Options

You can set up SigNoz in two ways:

1. **Cloud Setup**: Use SigNoz Cloud
2. **Local Setup**: Run SigNoz locally using Docker

## Option 1: SigNoz Cloud Setup

<Video src="https://www.youtube.com/watch?v=F8n6427hjUE&t=3s" title="SigNoz Cloud Setup steps" />

### Step 1: Create a SigNoz Cloud Project

1. Sign up at [SigNoz](https://signoz.io/teams/)
2. Create a new project or use an existing one

### Step 2: Save Your SigNoz Credentials

You'll need to collect three pieces of information from your SigNoz dashboard:

1. **API Key**:

   * Navigate to Settings → Workspace Settings → API Keys → New Key
   * Choose any role (Admin, Editor, or Viewer) - Viewer is sufficient for observability
   * Set the expiration field to "No Expiry" to prevent the key from expiring
   * Copy the generated API key

2. **Ingestion Key**:

   * Navigate to Settings → Workspace Settings → Ingestion
   * Set the expiration field to "No Expiry" to prevent the key from expiring
   * Copy the ingestion key

3. **SigNoz URL**:
   * Copy the URL from your browser's address bar
   * It will look like: `https://<your-organization>.signoz.cloud`

<Note>
  By default, the retention period for conversation data and traces is set to **15 days**. To set a longer retention period, navigate to the **General** tab on the **Settings** page in SigNoz.
</Note>

### Step 3: Configure Your Root `.env` File

```bash
# SigNoz
SIGNOZ_URL=https://<your-organization>.signoz.cloud
SIGNOZ_API_KEY=<your-api-key>

OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://ingest.us.signoz.cloud:443/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
```

### Step 4: Verify Cloud Setup

1. Restart your development environment:

   ```bash
   pnpm dev
   ```

2. Generate some traces by interacting with your agents

3. Open your SigNoz cloud dashboard and navigate to "Traces" to see your agent traces

## Option 2: Local SigNoz Setup

### Automated Setup (Recommended)

<Note>
  Run `pnpm setup-dev` first to set up core databases and migrations. `setup-dev:optional` only starts optional services.
</Note>

Run the following from your project root:

```bash
pnpm setup-dev:optional
```

This:

* Clones [`agents-optional-local-dev`](https://github.com/inkeep/agents-optional-local-dev) into `.optional-services/` if not already present
* Starts SigNoz, OTEL Collector, Jaeger, and [Nango](/typescript-sdk/credentials/nango) via Docker Compose
* Registers a SigNoz admin account and creates an API key
* Writes all required env vars to your `.env`

Restart your dev server after setup completes:

```bash
pnpm dev
```

Open SigNoz at `http://localhost:3080` to view traces. Jaeger is available at `http://localhost:16686`.

Use `pnpm optional:status` to check service health, `pnpm optional:stop` to stop optional services, or `pnpm optional:reset` to start fresh.

### Manual Setup

<Video src="https://www.youtube.com/watch?v=DWuL4AeRzAA&t=3s" title="Local SigNoz Setup steps" />

If you prefer manual setup:

#### Prerequisites

* Docker installed on your machine

#### Step 1: Clone the Optional Services Repository

Clone the Inkeep optional local development services repository:

```bash
git clone https://github.com/inkeep/agents-optional-local-dev .optional-services
cd .optional-services
```

#### Step 2: Start SigNoz Services

Run the following command to start SigNoz and related services:

```bash
docker compose --profile signoz up -d
```

This will start:

* SigNoz frontend (accessible at `http://localhost:3080`)
* SigNoz query service
* SigNoz OTEL collector
* ClickHouse database

When you visit `http://localhost:3080`, you can sign up with your desired credentials.

#### Step 3: Configure Environment Variables

In your **root project directory** (e.g., `my-agents`), update your `.env` file:

```bash
# SigNoz Configuration
SIGNOZ_URL=http://localhost:3080
SIGNOZ_API_KEY=your-signoz-api-key

# Local SigNoz OTEL endpoint (port 4318 — the SigNoz collector)
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces
OTEL_SERVICE_NAME=inkeep-agents
```

To get your SigNoz API key:

1. Open SigNoz at `http://localhost:3080`
2. Navigate to Settings → Account Settings → API Keys → New Key
3. Create a new API key or copy an existing one.
   * Choose any role (Admin, Editor, or Viewer) - Viewer is sufficient for observability
   * Set the expiration field to "No Expiry" to prevent the key from expiring

<Note>
  By default, the retention period for conversation data and traces is set to **15 days**. To set a longer retention period, navigate to the **General** tab on the **Settings** page in SigNoz.
</Note>

#### Step 4: Verify Setup

1. Restart your Inkeep agents:

   ```bash
   pnpm dev
   ```

2. Make some requests to your agents to generate traces

3. Open SigNoz at `http://localhost:3080` and navigate to the "Traces" section to see your agent traces

## Viewing Traces and Using the Live Debugger

Once SigNoz is set up, you can access traces and live debugging in two ways:

### 1. Visual Builder Traces Interface

If you're using the Visual Builder:

1. Open your agent project in the Visual Builder
2. Navigate to the **Traces** section
3. You'll see real-time traces of your agent executions
4. Click on any trace to see detailed execution flow and timing

The traces overview shows conversation metrics and recent activity:

<Image src="/images/trace-overview.png" alt="Traces overview dashboard showing conversation metrics and recent activity" />

Click on any conversation to see detailed execution flow:

<Image src="/images/trace-detail.png" alt="Detailed conversation trace showing step-by-step execution and timing" />

<>
  ### Copy Trace for Debugging

  The `Copy Trace` button in the timeline view allows you to export the entire conversation trace as JSON. This is particularly useful for offline analysis and debugging complex flows.

  <Image src="/images/copy-trace.png" alt="Copy Trace button in the timeline view for exporting conversation traces" />

  #### What's Included in the Trace Export

  When you click `Copy Trace`, the system exports a JSON object containing:

  ```json
  {
    "metadata": {
      "conversationId": "unique-conversation-id",
      "traceId": "distributed-trace-id",
      "agentId": "agent-identifier",
      "agentName": "Agent Name",
      "exportedAt": "2025-10-14T12:00:00.000Z"
    },
    "timing": {
      "startTime": "2025-10-14T11:59:00.000Z",
      "endTime": "2025-10-14T12:00:00.000Z",
      "durationMs": 60000
    },
    "timeline": [
      // Array of all activities with complete details:
      // - Agent messages and responses
      // - Tool calls and results
      // - Agent transfers
      // - Artifact information
      // - Execution context
    ]
  }
  ```

  #### How to Use Copy Trace

  1. Navigate to the **Traces** section in the management UI
  2. Open the conversation you want to debug
  3. Click the **Copy Trace** button at the top of the timeline
  4. The complete trace JSON is copied to your clipboard
  5. Paste it into your preferred tool for analysis

  This exported trace contains all the activities shown in the timeline, making it easy to share complete execution context with team members or support.
</>

#### Time to first token (TTFT)

Each conversation records how quickly the agent started responding. The conversation detail view shows the **time to first token** — the time from request arrival to the first user-visible text token.

Three interaction-level timings are recorded as span attributes (in seconds) on the request span, and surfaced in the assistant message's span panel:

* `inkeep.agent.time_to_first_model_token` — first raw token from the model (model + network latency).
* `inkeep.agent.time_to_first_visible_token` — first user-visible **text** token (this is the headline "time to first token"). Omitted for turns that never produce text (for example, tool-only or artifact-only responses).
* `inkeep.agent.time_to_first_visible_part` — first user-visible output of **any** kind (text, a tool card, or an artifact). On a typical text-first turn this matches the visible-token timing; it diverges only when a tool card or artifact renders before any text.

The gap between the model timing and the visible-text timing reflects the framework's own parsing and buffering overhead. All three are queryable in SigNoz by `conversation.id` and `agent.id`.

### 2. SigNoz Dashboard

For detailed analysis and further debugging:

1. Open your SigNoz dashboard (cloud or local)
2. Navigate to **Traces** to see all agent executions
3. Use filters to find specific conversations or agents
4. Click on traces to see:
   * Step-by-step execution details
   * Performance metrics
   * Error information
   * Agent-to-agent communication flows

For more detailed information on using traces, see the [SigNoz Usage guide](/guides/observability/signoz-usage).

## Additional Observability and Evals

👉 For additional observability or a dedicated Evals platform, you can connect to any OTEL-based provider. For example, check out the [Langfuse Usage guide](/guides/observability/langfuse-usage) for end-to-end instructions.

## Next steps

Next, we recommend setting up the Nango credential store for production-ready credential management. See [Credentials](/typescript-sdk/credentials/overview) to get started.


# CrewAI vs Inkeep (/comparisons/crewai)

Compare CrewAI with Inkeep



## Overview

CrewAI is a Python-only, developer-focused platform with process-based (sequential/hierarchical) agent orchestration, whereas Inkeep provides true autonomous agents with 2-way code-UI sync, out-of-box chat components, native data ingestion, and ready-to-deploy integrations for customer-facing AI experiences.

### Building Agents

<ComparisonTable competitor="crewai" sectionTitle="Building Agents" />

### Developer Platform

<ComparisonTable competitor="crewai" sectionTitle="Developer Platform" />

### Unified AI Search & RAG

<ComparisonTable competitor="crewai" sectionTitle="Unified AI Search & RAG" />

### Interact with your AI Agents in...

<ComparisonTable competitor="crewai" sectionTitle="Interact with your AI Agents in..." />

### Building Agent UIs

<ComparisonTable competitor="crewai" sectionTitle="Building Agent UIs" />

### AI Agents for...

<ComparisonTable competitor="crewai" sectionTitle="AI Agents for..." />

### Insights & Analytics

<ComparisonTable competitor="crewai" sectionTitle="Insights & Analytics" />

### Authentication and Authorization

<ComparisonTable competitor="crewai" sectionTitle="Authentication and Authorization" />

### Deployment

<ComparisonTable competitor="crewai" sectionTitle="Deployment" />

### Security

<ComparisonTable competitor="crewai" sectionTitle="Security" />


# Lindy vs Inkeep (/comparisons/lindy)

Compare Lindy with Inkeep



## Overview

Inkeep is a developer-first platform with a TypeScript SDK, 2-way code-UI sync, and graph-based multi-agent orchestration for building sophisticated AI systems, while Lindy is a no-code workflow automation tool designed for business users who prefer visual-only configuration.

### Building Agents

<ComparisonTable competitor="Lindy" sectionTitle="Building Agents" />

### Developer Platform

<ComparisonTable competitor="Lindy" sectionTitle="Developer Platform" />

### Unified AI Search & RAG

<ComparisonTable competitor="Lindy" sectionTitle="Unified AI Search & RAG" />

### Interact with your AI Agents in...

<ComparisonTable competitor="Lindy" sectionTitle="Interact with your AI Agents in..." />

### Building Agent UIs

<ComparisonTable competitor="Lindy" sectionTitle="Building Agent UIs" />

### AI Agents for...

<ComparisonTable competitor="Lindy" sectionTitle="AI Agents for..." />

### Insights & Analytics

<ComparisonTable competitor="Lindy" sectionTitle="Insights & Analytics" />

### Authentication and Authorization

<ComparisonTable competitor="Lindy" sectionTitle="Authentication and Authorization" />

### Deployment

<ComparisonTable competitor="Lindy" sectionTitle="Deployment" />

### Security

<ComparisonTable competitor="Lindy" sectionTitle="Security & Compliance" />


# n8n vs Inkeep (/comparisons/n8n)

Compare n8n with Inkeep



## Overview

n8n excels at deterministic, rule-based workflow automation where each step follows a predefined path. Inkeep, by contrast, is built for agentic workflows and conversational AI-driven systems that reason, adapt, and make decisions dynamically.

### Building Agents

<ComparisonTable competitor="n8n" sectionTitle="Building Agents" />

### Workflow Automation

<ComparisonTable competitor="n8n" sectionTitle="Workflow Automation" />

### Developer Platform

<ComparisonTable competitor="n8n" sectionTitle="Developer Platform" />

### Unified AI Search & RAG

<ComparisonTable competitor="n8n" sectionTitle="Unified AI Search & RAG" />

### Interact with your AI Agents in...

<ComparisonTable competitor="n8n" sectionTitle="Interact with your AI Agents in..." />

### Building Agent UIs

<ComparisonTable competitor="n8n" sectionTitle="Building Agent UIs" />

### AI Agents for...

<ComparisonTable competitor="n8n" sectionTitle="AI Agents for..." />

### Insights & Analytics

<ComparisonTable competitor="n8n" sectionTitle="Insights & Analytics" />

### Authentication and Authorization

<ComparisonTable competitor="n8n" sectionTitle="Authentication and Authorization" />

### Deployment

<ComparisonTable competitor="n8n" sectionTitle="Deployment" />

### Security

<ComparisonTable competitor="n8n" sectionTitle="Security" />


# OpenAI AgentKit vs Inkeep (/comparisons/openai-agent-kit)

Compare OpenAI AgentKit with Inkeep



## Overview

OpenAI AgentKit provides strong UI components (ChatKit) and basic agent building, but limits you to OpenAI models with manual knowledge management. Inkeep offers multi-agent orchestration, 2-way code-UI sync, automated knowledge ingestion, multi-provider support, and enterprise-grade content intelligence.

### Building Agents

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Building Agents" />

### Developer Platform

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Developer Platform" />

### Unified AI Search & RAG

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Unified AI Search & RAG" />

### Interact with your AI Agents in...

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Interact with your AI Agents in..." />

### Building Agent UIs

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Building Agent UIs" />

### AI Agents for...

<ComparisonTable competitor="openai-agent-kit" sectionTitle="AI Agents for..." />

### Insights & Analytics

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Insights & Analytics" />

### Authentication and Authorization

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Authentication and Authorization" />

### Deployment

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Deployment" />

### Security

<ComparisonTable competitor="openai-agent-kit" sectionTitle="Security" />


# Upgrading your Inkeep version (/guides/upgrading)

Upgrade the packages that make up the Inkeep Agent Framework



## Overview

The Inkeep Agent Framework is composed of several npm packages:

* [`@inkeep/agents-api`](https://www.npmjs.com/package/@inkeep/agents-api): The unified API for managing projects, agent configurations and executing conversations with your agents.
* [`@inkeep/agents-manage-ui`](https://www.npmjs.com/package/@inkeep/agents-manage-ui): The UI for the visual builder and dashboard.
* [`@inkeep/agents-core`](https://www.npmjs.com/package/@inkeep/agents-core): The core shared functionality of the agent framework.
* [`@inkeep/agents-sdk`](https://www.npmjs.com/package/@inkeep/agents-sdk): The TypeScript SDK for building multi-agent systems.
* [`@inkeep/agents-cli`](https://www.npmjs.com/package/@inkeep/agents-cli): The CLI for managing and interacting with the agent framework.
* [`@inkeep/agents-ui-cloud`](https://www.npmjs.com/package/@inkeep/agents-ui-cloud): The UI library containing chat widget components.

## Upgrading the quickstart template

<Warning>
  Because the packages are pre-1.0.0, updates may contain breaking changes that require you to reset your database. Please upgrade at your own risk.
</Warning>

If you used the `npx @inkeep/create-agents` CLI command to create your workspace, run the following command from the workspace root:

```bash
pnpm upgrade-agents
```

This will update all the packages to the latest version and migrate your database schema to the latest version.

<Note>
  Your Docker databases must be running before upgrading. If they aren't running, run `pnpm setup-dev` first, then run `pnpm upgrade-agents`.
</Note>

### Troubleshooting:

If you encounter migration errors during the upgrade, this indicates a breaking schema change. You'll need to reset your database:

1. Drop your database: `pnpm db:drop`
2. Try again: `pnpm upgrade-agents`

## Upgrading the Agent CLI

If you installed the `@inkeep/agents-cli` package globally, you can upgrade it to the latest version by running the following command:

```bash
inkeep update
```


# Talk to your agent via A2A (JSON-RPC) (/talk-to-your-agents/a2a)

Use the Agent2Agent JSON-RPC protocol to send messages to your agent and receive results, with optional streaming.



The A2A (Agent-to-Agent) endpoint lets third-party agents, agent platforms, or agent workspaces interact with your Inkeep Agent using a standard agent protocol.

Here are some example platforms that you can add Inkeep Agents to:

| Platform                                                                                                                                                                                                       | Description                                                                                             |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| **[Google Gemini Enterprise](https://cloud.google.com/gemini-enterprise/faq)**                                                                                                                                 | Bring‑your‑own A2A agents into an enterprise agentspace and orchestrate alongside Google/vendor agents. |
| **[Microsoft Copilot Studio / Azure AI Foundry](https://microsoftlearning.github.io/mslearn-ai-agents/Instructions/06-multi-remote-agents-with-a2a.html)**                                                     | Copilots can invoke external A2A agents as peer services in multi‑agent flows.                          |
| **[Salesforce Agentforce](https://architect.salesforce.com/fundamentals/agentic-patterns)**                                                                                                                    | Add third‑party A2A agents (e.g., via AgentExchange) and compose them in CRM workflows.                 |
| **[SAP Joule](https://learning.sap.com/courses/boosting-ai-driven-business-transformation-with-joule-agents/enabling-interoperability-for-ai-agents)**                                                         | Federate non‑SAP A2A agents into SAP’s business agent fabric.                                           |
| **[ServiceNow AI Agent Fabric](https://www.servicenow.com/community/now-assist-articles/introducing-ai-agent-fabric-enable-mcp-and-a2a-for-your-agentic/ta-p/3373907)**                                        | Discover and call external A2A agents within IT/business automations.                                   |
| **[Atlassian Rovo](https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-mcp-server/)**                                                                       | Configure Rovo to call external A2A agents for cross‑tool tasks.                                        |
| **[Workday AI (Agent Gateway / ASOR)](https://investor.workday.com/2025-06-03-Workday-Announces-New-AI-Agent-Partner-Network-and-Agent-Gateway-to-Power-the-Next-Generation-of-Human-and-Digital-Workforces)** | Register external customer/partner A2A agents alongside Workday agents.                                 |

<>
  ## Authentication

  Choose the authentication method:

  <Tabs>
    <Tab title="App Credential (app-based)">
      App credentials are the auth method for app-based integrations such as web clients, support copilots, and authenticated end-user experiences. For web clients, the App ID is safe to expose client-side.

      **Setup:**

      1. [Create an app credential](/talk-to-your-agents/app-credentials#create-an-app-via-api) for your project
      2. Request an anonymous session token from your client
      3. Use the token and App ID in chat requests

      **Request Headers:**

      ```http
      Authorization: Bearer <session_token>
      X-Inkeep-App-Id: <app_id>
      ```

      See [App Credentials](/talk-to-your-agents/app-credentials) for the full auth flow details.
    </Tab>

    <Tab title="API Key (server-to-server)">
      Use API keys for server-to-server integrations where the secret is never exposed to end-users.

      **Create an API Key:**

      1. Open the Visual Builder Dashboard
      2. Go to your Project → **API Keys**
      3. Click **Create**, select the target agent
      4. Copy the API key (it will be shown once) and store it securely

      **Request Header:**

      ```http
      Authorization: Bearer <api_key>
      ```

      <Warning>
        API keys are secrets. Never expose them in client-side code or browser requests.
      </Warning>
    </Tab>

    <Tab title="Development (no auth)">
      When running the API server locally with `pnpm dev`, authentication is automatically bypassed. You can use headers in the request instead:

      **Request Headers:**

      ```http
      x-inkeep-tenant-id: <tenant-id>
      x-inkeep-project-id: <project-id>
      x-inkeep-agent-id: <agent-id>
      ```

      <Warning>
        This mode is for development only. Never use in production as it bypasses all security checks.
      </Warning>
    </Tab>
  </Tabs>

  See [Authentication → Run API](/api-reference/authentication/run-api) for more details.
</>

## Endpoints

* **Agent card discovery (agent or sub-agent-level):** `GET <INKEEP_AGENTS_API_URL>/run/agents/.well-known/agent.json`
* **A2A protocol (agent or sub-agent-level):** `POST <INKEEP_AGENTS_API_URL>/run/agents/.well-known/agent.json`

Notes:

* If you supply `x-inkeep-sub-agent-id` in headers, requests target that specific Sub Agent. This is supported in development (or when using the bypass secret). With production API keys, requests always use the Agent's default Sub Agent.

## Message Send (Blocking)

* **Path:** `POST  <INKEEP_AGENTS_API_URL>/run/agents/a2a`
* **Headers:** Per Authentication section (Standard API Key recommended)
* **Body (JSON-RPC 2.0):**

```json
{
  "jsonrpc": "2.0",
  "method": "message/send",
  "id": 1,
  "params": {
    "message": {
      "messageId": "msg-123",
      "role": "user",
      "parts": [
        { "kind": "text", "text": "Hello!" }
      ],
      "contextId": "conv-123",
      "kind": "message"
    },
    "configuration": {
      "acceptedOutputModes": ["text", "text/plain"],
      "blocking": true
    }
  }
}
```

* **Success Response (Message):**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "kind": "message",
    "messageId": "auto-generated",
    "role": "agent",
    "parts": [ { "kind": "text", "text": "..." } ],
    "taskId": "task-...",
    "contextId": "conv-123"
  },
  "id": 1
}
```

* **Transfer Case:** if the agent decides to transfer, the response contains a `task` with a transfer artifact:

```json
{
  "jsonrpc": "2.0",
  "result": {
    "kind": "task",
    "id": "task-...",
    "contextId": "conv-123",
    "status": { "state": "completed", "timestamp": "..." },
    "artifacts": [
      {
        "artifactId": "...",
        "parts": [
          { "kind": "data", "data": { "type": "transfer", "targetSubAgentId": "other-agent-id" } },
          { "kind": "text", "text": "Transfer reason text" }
        ]
      }
    ]
  },
  "id": 1
}
```

## Message Send (Non-blocking)

Set `configuration.blocking` to `false`. The server immediately returns a `task`, and you can poll or stream updates.

* **Request:** same as above, with `"blocking": false`
* **Response (Task):**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "kind": "task",
    "id": "task-...",
    "contextId": "conv-123",
    "status": { "state": "completed", "timestamp": "..." },
    "artifacts": [ { "artifactId": "...", "parts": [ { "kind": "text", "text": "..." } ] } ]
  },
  "id": 1
}
```

## Streaming Messages (SSE)

* **Path:** `POST /run/agents/a2a`
* **Headers:** include `Accept: text/event-stream` plus Authentication headers
* **Body (JSON-RPC 2.0):** same as blocking, but method `"message/stream"`

```json
{
  "jsonrpc": "2.0",
  "method": "message/stream",
  "id": 2,
  "params": {
    "message": {
      "messageId": "msg-456",
      "role": "user",
      "parts": [ { "kind": "text", "text": "Stream please" } ],
      "contextId": "conv-123",
      "kind": "message"
    },
    "configuration": {
      "acceptedOutputModes": ["text", "text/plain"],
      "blocking": false
    }
  }
}
```

* **SSE Events (each line is an SSE `data:` payload containing JSON-RPC):**

```text
: keep-alive

data: {"jsonrpc":"2.0","result":{"kind":"task","id":"task-...","contextId":"conv-123","status":{"state":"working","timestamp":"..."},"artifacts":[]},"id":2}

data: {"jsonrpc":"2.0","result":{"kind":"message","messageId":"...","role":"agent","parts":[{"kind":"text","text":"..."}],"taskId":"task-...","contextId":"conv-123"},"id":2}

data: {"jsonrpc":"2.0","result":{"kind":"task","id":"task-...","contextId":"conv-123","status":{"state":"completed","timestamp":"..."},"artifacts":[{"artifactId":"...","parts":[{"kind":"text","text":"..."}]}]},"id":2}
```

* **Transfer (streaming):** if a transfer is triggered, an SSE event with a JSON-RPC `result` of transfer details is sent, then the stream ends.

## Task APIs

* **Get task:** `POST /run/agents/a2a` with method `"tasks/get"` and params `{ "id": "task-..." }` → returns the `task`
* **Cancel task:** `POST /run/agents/a2a` with method `"tasks/cancel"` and params `{ "id": "task-..." }` → returns `{ "success": true }`
* **Resubscribe (SSE, mock):** `POST /run/agents/a2a` with method `"tasks/resubscribe"` and params `{ "taskId": "task-..." }` → SSE with a task event

<Note>
  Currently, `tasks/get` and `tasks/cancel` return stubbed responses, and `tasks/resubscribe` returns a mock SSE event. For live progress updates, use `message/stream`.
</Note>

## Agent Card Discovery

* **Agent-level:** `GET /run/agents/.well-known/agent.json` (uses Agent's default Sub Agent)
* **Agent-level (dev/bypass only):** Provide `x-inkeep-sub-agent-id` in headers to target a specific agent for discovery

## Notes & Behavior

* **contextId resolution:** The server first tries `task.context.conversationId` (derived from the request), then `params.message.metadata.conversationId`. Final fallback is `'default'`.
* **Artifacts in responses:** Message/Task responses may include `artifacts[0].parts` as the agent's output parts.
* **Errors (JSON-RPC):** Standard JSON-RPC error codes: `-32600`, `-32601`, `-32602`, `-32603`, `-32700`, plus A2A-specific `-3200x` codes.

## Development Notes

* **Base URL (local):** `http://localhost:3002`
* **Route Mounting:** A2A routes are mounted under `/run/agents`; use `/run/agents/a2a` for RPC and `/run/agents/.well-known/agent.json` for discovery
* **Streaming support:** Requires agent capabilities `streaming: true` in the agent card


# How to call your AI Agent using the Chat API (/talk-to-your-agents/chat-api)

Learn about details of the Vercel AI SDK data stream protocol that powers the `/run/api/chat` API endpoint.



## Overview

This guide shows how to call your agent directly over HTTP and stream responses using the Vercel AI SDK data stream format. It covers the exact endpoint, headers, request body, and the event stream response you should expect.

<Tip>
  If you are building a React UI, consider our prebuilt components under [React
  UI Components](/talk-to-your-agents/react/chat-button) or [Vercel AI
  Elements](/talk-to-your-agents/vercel-ai-sdk/ai-elements) headless primitives.
  This page is for the low-level streaming API.
</Tip>

## Endpoint

* **Path (mounted by the Run API):** `/run/api/chat`
* **Method:** `POST`
* **Protocol:** Server-Sent Events (SSE) encoded JSON, using Vercel AI SDK data-stream v2
* **Content-Type (response):** `text/event-stream`
* **Response Header:** `x-vercel-ai-data-stream: v2`

<>
  ## Authentication

  Choose the authentication method:

  <Tabs>
    <Tab title="App Credential (app-based)">
      App credentials are the auth method for app-based integrations such as web clients, support copilots, and authenticated end-user experiences. For web clients, the App ID is safe to expose client-side.

      **Setup:**

      1. [Create an app credential](/talk-to-your-agents/app-credentials#create-an-app-via-api) for your project
      2. Request an anonymous session token from your client
      3. Use the token and App ID in chat requests

      **Request Headers:**

      ```http
      Authorization: Bearer <session_token>
      X-Inkeep-App-Id: <app_id>
      ```

      See [App Credentials](/talk-to-your-agents/app-credentials) for the full auth flow details.
    </Tab>

    <Tab title="API Key (server-to-server)">
      Use API keys for server-to-server integrations where the secret is never exposed to end-users.

      **Create an API Key:**

      1. Open the Visual Builder Dashboard
      2. Go to your Project → **API Keys**
      3. Click **Create**, select the target agent
      4. Copy the API key (it will be shown once) and store it securely

      **Request Header:**

      ```http
      Authorization: Bearer <api_key>
      ```

      <Warning>
        API keys are secrets. Never expose them in client-side code or browser requests.
      </Warning>
    </Tab>

    <Tab title="Development (no auth)">
      When running the API server locally with `pnpm dev`, authentication is automatically bypassed. You can use headers in the request instead:

      **Request Headers:**

      ```http
      x-inkeep-tenant-id: <tenant-id>
      x-inkeep-project-id: <project-id>
      x-inkeep-agent-id: <agent-id>
      ```

      <Warning>
        This mode is for development only. Never use in production as it bypasses all security checks.
      </Warning>
    </Tab>
  </Tabs>

  See [Authentication → Run API](/api-reference/authentication/run-api) for more details.
</>

## Request Body Schema

```json
{
  "messages": [
    {
      "role": "user",
      "content": "Hello!"
    }
  ],
  "conversationId": "optional-conversation-id"
}
```

**Field Notes:**

* **`messages`** — Must include at least one `user` message
* **`content`** — Optional string message text (legacy-compatible)
* **`parts`** — Optional multi-part content array (text + images)
* **`content` + `parts` together** — Both are accepted. `content` is prepended as a text part unless an identical text part already exists.
* **`conversationId`** — Optional; server generates one if omitted

## File inputs (images, PDFs, and text documents)

The chat endpoint supports files in message content parts with different rules by type:

* **Images (`image_url`)**: support either remote URLs (`https://...`) or base64 `data:` URIs
* **Files (`file`)**: support PDFs plus inline UTF-8 text/code/config document formats such as plain text, Markdown, HTML, CSV, logs, JSON, JavaScript, XML, YAML (`.yaml` and `.yml`), CSS, shell scripts, source files, templates, subtitles, calendars, vCards, and RFC 822 email messages
* **PDF file inputs**: support either remote URLs (`https://...`) or base64 `data:` payloads
* **Text document file inputs**: support inline base64 `data:` payloads only. Remote URLs are not accepted for supported text/code document MIME types.

### Request shape for file parts

```json
{
  "messages": [
    {
      "role": "user",
      "content": "Summarize these attachments",
      "parts": [
        {
          "type": "text",
          "text": "Summarize these attachments"
        },
        {
          "type": "image",
          "text": "https://example.com/image.png"
        },
        {
          "type": "image",
          "text": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
        },
        {
          "type": "file",
          "url": "data:application/pdf;base64,JVBERi0xLjQKJcTl8uXr...",
          "mediaType": "application/pdf",
          "filename": "report.pdf"
        },
        {
          "type": "file",
          "url": "data:text/plain;base64,SGVsbG8gZnJvbSBhIHR4dCBhdHRhY2htZW50Lg==",
          "mediaType": "text/plain",
          "filename": "notes.txt"
        },
        {
          "type": "file",
          "url": "data:text/markdown;base64,IyBUaXRsZQoKLSBpdGVtIDE=",
          "mediaType": "text/markdown",
          "filename": "notes.md"
        },
        {
          "type": "file",
          "url": "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=",
          "mediaType": "text/html",
          "filename": "page.html"
        },
        {
          "type": "file",
          "url": "data:text/csv;base64,bmFtZSxjb3VudAphbHBoYSwxCg==",
          "mediaType": "text/csv",
          "filename": "data.csv"
        },
        {
          "type": "file",
          "url": "data:text/x-log;base64,W2luZm9dIGJvb3QKW2Vycm9yXSBmYWlsdXJlCg==",
          "mediaType": "text/x-log",
          "filename": "server.log"
        },
        {
          "type": "file",
          "url": "data:application/json;base64,eyJuYW1lIjoiYWxwaGEiLCJjb3VudCI6MX0=",
          "mediaType": "application/json",
          "filename": "data.json"
        },
        {
          "type": "file",
          "url": "data:application/javascript;base64,ZXhwb3J0IGNvbnN0IGdyZWV0aW5nID0gImhlbGxvIjsK",
          "mediaType": "application/javascript",
          "filename": "snippet.js"
        },
        {
          "type": "file",
          "url": "data:application/yaml;base64,bmFtZTogYWxwaGEKY291bnQ6IDEK",
          "mediaType": "application/yaml",
          "filename": "config.yml"
        },
        {
          "type": "file",
          "url": "https://example.com/document.pdf",
          "mediaType": "application/pdf",
          "filename": "document.pdf"
        }
      ]
    }
  ]
}
```

### Base64 and size requirements

* **Encoding**: data URIs must include a valid base64 payload
* **Maximum size**:
  * Images and PDFs: each inline file payload is limited to **10 MB**
  * Text/code/config documents: each inline file payload is limited to **256 KB**
* **Vercel `parts` file typing**: for `/run/api/chat`, file parts use Vercel-compatible shape
  with `url` (data URI or `https://` for PDFs) and required `mediaType`
* **Content validation**:
  * Images are validated against supported image bytes
  * PDFs must start with the `%PDF-` signature bytes
  * Text/code/config documents must decode as valid UTF-8 text and are rejected if they contain binary-style control characters
* **Model input handling for text documents**: text attachments are injected into the prompt only on the turn where the user sends them; persisted text attachments are not re-injected from later conversation history
* **HTML semantics**: `text/html` files are treated as raw source text. The API does not render, sanitize, or convert HTML to visible page text before injection.
* **Remote PDF URLs**: must be reachable over `http` or `https` from the Run API. Unreachable hosts, non-PDF responses, or oversized bodies result in **HTTP 400** with an error message (query strings and fragments on the URL are not preserved in stored metadata).
* **Remote text document URLs**: are not supported. Use inline base64 `data:` payloads for supported text/code document MIME types instead.

### Optional Headers

* **`x-emit-operations`** — Set to `true` to include detailed data operations in the response stream. Useful for debugging and monitoring agent behavior. See [Data Operations](/typescript-sdk/data-operations) for details.

### Example cURL

When using an API key for auth:

```bash
curl -N \
  -X POST "http://localhost:3002/run/api/chat" \
  -H "Authorization: Bearer $INKEEP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "x-emit-operations: true" \
  -d '{
    "messages": [
      { "role": "user", "content": "What can you do?" }
    ],
    "conversationId": "chat-1234"
  }'
```

## Response: Vercel AI SDK Data Stream (v2)

The response is an SSE stream of JSON events compatible with the Vercel AI SDK UI message stream. The server sets `x-vercel-ai-data-stream: v2`.

### Event Types

#### Text Streaming Events

* **`text-start`** — Indicates a new text segment is starting
* **`text-delta`** — Carries the text content delta for the current segment
* **`text-end`** — Marks the end of the current text segment

#### Data Events

* **`data-component`** — Structured UI data emitted by the agent (for rich UIs)
* **`data-artifact`** — Artifact data emitted by tools/agents (documents, files, saved results)
* **`data-operation`** — Low-level operational events (agent lifecycle, completion, errors)
* **`data-summary`** — AI-generated status updates with user-friendly labels and contextual details

#### Tool Events

* **`tool-input-start`**, **`tool-input-delta`**, **`tool-input-available`** — Tool input streaming
* **`tool-approval-request`** — Emitted when a tool requires user approval before execution
* **`tool-output-available`**, **`tool-output-denied`** — Tool output (including approval denial)

### Tool approval

If a tool is configured with `needsApproval: true`, the run pauses and the stream includes a `tool-approval-request` event:

```json
{
  "type": "tool-approval-request",
  "toolCallId": "call_abc123def456",
  "approvalId": "aitxt-call_abc123def456"
}
```

To continue the run, your client must approve/deny the pending tool call. See [Tool approvals](/typescript-sdk/tools/tool-approvals).

### Tool input & output events

Depending on UI needs, the stream may include tool input and tool output events.

```json
{
  "type": "tool-input-start",
  "toolCallId": "call_abc123def456",
  "toolName": "get_coordinates"
}
```

```json
{
  "type": "tool-input-delta",
  "toolCallId": "call_abc123def456",
  "inputTextDelta": "{\"address\":\"San"
}
```

```json
{
  "type": "tool-input-available",
  "toolCallId": "call_abc123def456",
  "toolName": "get_coordinates",
  "input": { "address": "San Francisco" }
}
```

```json
{
  "type": "tool-output-available",
  "toolCallId": "call_abc123def456",
  "output": { "lat": 37.7749, "lng": -122.4194 }
}
```

```json
{ "type": "tool-output-denied", "toolCallId": "call_abc123def456" }
```

### Example Stream (abbreviated)

```text
: keep-alive

data: {"type":"text-start","id":"1726247200-abc123"}

data: {"type":"text-delta","id":"1726247200-abc123","delta":"Hello! I can help with..."}

data: {"type":"data-summary","data":{"label":"Searching documentation","details":{"summary":"Looking for relevant information","progress":"25%"}}}

data: {"type":"text-delta","id":"1726247200-abc123","delta":" analyzing your query..."}

data: {"type":"text-end","id":"1726247200-abc123"}

data: {"type":"data-operation","data":{"type":"completion","ctx":{"agent":"search-agent","iteration":1}}}

data: {"type":"data-component","id":"1726247200-abc123-0","data":{"type":"customer-info","name":"Ada","email":"ada@example.com"}}

data: {"type":"data-artifact","data":{"artifact_id":"art_abc","task_id":"task_xyz","summary":{"title":"Search Results"}}}
```

### Data Event Details

#### `data-operation` Events

Low-level operational events with technical context. Common types include:

* **`agent_initializing`** — The agent runtime is starting
* **`agent_ready`** — Agent is ready and processing
* **`completion`** — The agent completed the task (includes agent ID and iteration count)
* **`error`** — Error information (also emitted as a top-level `error` event)

```json
{
  "type": "data-operation",
  "data": {
    "type": "completion",
    "ctx": { "agent": "search-agent", "iteration": 1 }
  }
}
```

#### `data-summary` Events

AI-generated status updates designed for end-user consumption. Structure:

* **`label`** — User-friendly description (required)
* **`details`** — Optional structured/unstructured context data

```json
{
  "type": "data-summary",
  "data": {
    "label": "Processing search results",
    "details": {
      "summary": "Found 12 relevant documents",
      "itemsProcessed": 12,
      "status": "analyzing"
    }
  }
}
```

#### `data-artifact` Events

Emitted when an agent saves an artifact from a tool result. The event carries the artifact's **preview fields** in `summary` — these are the fields marked `inPreview: true` in the artifact schema and are available immediately in the agent's context.

```json
{
  "type": "data-artifact",
  "data": {
    "artifact_id": "art_123",
    "task_id": "task_456",
    "summary": { "title": "Weather Report", "type": "document" }
  }
}
```

The complete artifact data (including non-preview fields) is persisted and can be fetched on demand. See [Artifact Components](/typescript-sdk/structured-outputs/artifact-components) for details on preview vs. non-preview fields.

#### `data-component` Events

Structured UI data for rich interface components:

```json
{
  "type": "data-component",
  "id": "comp_123",
  "data": { "type": "chart", "title": "Sales Data", "chartData": [1, 2, 3] }
}
```

### Text Streaming Behavior

* For each text segment, the server emits `text-start` → `text-delta` → `text-end`
* The server avoids splitting content word-by-word; a segment is usually a coherent chunk
* Operational events are queued during active text emission and flushed shortly after to preserve ordering and readability

## Error Responses

### Streamed Errors

Errors are now delivered as `data-operation` events with unified structure:

```json
{
  "type": "data-operation",
  "data": {
    "type": "error",
    "message": "Error description",
    "agent": "agent-id",
    "severity": "error",
    "code": "optional-error-code",
    "timestamp": 1640995200000
  }
}
```

### Non-Streaming Errors

Validation failures and other errors return JSON with an appropriate HTTP status code.

## HTTP Status Codes

* **`200`** — Stream opened successfully
* **`401`** — Missing/invalid authentication
* **`404`** — Agent not found
* **`400`** — Invalid request body/context
* **`500`** — Internal server error

## Development Notes

* **Default local base URL:** `http://localhost:3002`
* **Endpoint mounting in the server:**
  * `/run/api/chat` → Vercel data stream (this page)
  * `/run/v1/mcp` → MCP JSON-RPC endpoint

<Note>
  To test quickly without a UI, use `curl -N` or a tool that supports
  Server-Sent Events.
</Note>


# Logging events with the Events API (/talk-to-your-agents/events)

Log application-level events for the agents framework using the existing run-API auth.



## Overview

The Events API is a write-only ingest endpoint for application-level events emitted from any context that already authenticates against the run API: browser widgets, server-side agent code, or external SDK consumers. Events are persisted to the run-database, scoped per project, and serve as the source of truth for downstream analytics and outbound webhook delivery.

## Endpoint

* **Path:** `/run/v1/events`
* **Method:** `POST`
* **Auth:** Existing run-API strategies (API key or OAuth bearer token via the support\_copilot app credential path).
* **Content-Type:** `application/json`

<>
  ## Authentication

  Choose the authentication method:

  <Tabs>
    <Tab title="App Credential (app-based)">
      App credentials are the auth method for app-based integrations such as web clients, support copilots, and authenticated end-user experiences. For web clients, the App ID is safe to expose client-side.

      **Setup:**

      1. [Create an app credential](/talk-to-your-agents/app-credentials#create-an-app-via-api) for your project
      2. Request an anonymous session token from your client
      3. Use the token and App ID in chat requests

      **Request Headers:**

      ```http
      Authorization: Bearer <session_token>
      X-Inkeep-App-Id: <app_id>
      ```

      See [App Credentials](/talk-to-your-agents/app-credentials) for the full auth flow details.
    </Tab>

    <Tab title="API Key (server-to-server)">
      Use API keys for server-to-server integrations where the secret is never exposed to end-users.

      **Create an API Key:**

      1. Open the Visual Builder Dashboard
      2. Go to your Project → **API Keys**
      3. Click **Create**, select the target agent
      4. Copy the API key (it will be shown once) and store it securely

      **Request Header:**

      ```http
      Authorization: Bearer <api_key>
      ```

      <Warning>
        API keys are secrets. Never expose them in client-side code or browser requests.
      </Warning>
    </Tab>

    <Tab title="Development (no auth)">
      When running the API server locally with `pnpm dev`, authentication is automatically bypassed. You can use headers in the request instead:

      **Request Headers:**

      ```http
      x-inkeep-tenant-id: <tenant-id>
      x-inkeep-project-id: <project-id>
      x-inkeep-agent-id: <agent-id>
      ```

      <Warning>
        This mode is for development only. Never use in production as it bypasses all security checks.
      </Warning>
    </Tab>
  </Tabs>

  See [Authentication → Run API](/api-reference/authentication/run-api) for more details.
</>

## Request body

```json
{
  "id": "optional-client-supplied-id",
  "type": "user_message_submitted",
  "agentId": "support-agent",
  "conversationId": "conv-abc",
  "messageId": "msg-xyz",
  "properties": { "source": "chat-widget" },
  "userProperties": { "userId": "u_123", "plan": "pro" },
  "metadata": { "appVersion": "1.4.2" }
}
```

| Field                         | Required | Notes                                                                                                                                                                                                                                                                                    |
| ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                        | yes      | Free-form string. Use the well-known lifecycle event names (below) for widget-emitted events; consumers may use any other names.                                                                                                                                                         |
| `id`                          | no       | Client-supplied ID enables idempotency (see Idempotency below). Server generates one if omitted.                                                                                                                                                                                         |
| `agentId`                     | no       | Server fills from the authenticated execution context if absent.                                                                                                                                                                                                                         |
| `conversationId`, `messageId` | no       | Anchor the event to a specific conversation or message. Both are independently optional — events can be free-form (no anchor).                                                                                                                                                           |
| `properties`                  | no       | Free-form JSON object describing the event.                                                                                                                                                                                                                                              |
| `userProperties`              | no       | Free-form JSON object identifying the end-user. See [User attribution](#user-attribution) for recommended field conventions.                                                                                                                                                             |
| `metadata`                    | no       | Free-form JSON object — the **caller's namespace**. The server never reads or writes to this column. Any keys you put here (including a key called `authMethod`) are stored as-is. Server-controlled debug context lives in a separate `serverMetadata` column on responses (see below). |

## Responses

| Status             | Meaning                                                                                                                                   |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `201 Created`      | A new event row was inserted.                                                                                                             |
| `200 OK`           | An event with the supplied `id` already existed for `(tenantId, projectId)`. The persisted row is returned unchanged (idempotent insert). |
| `400 Bad Request`  | The request body failed validation (most commonly: missing `type`).                                                                       |
| `401 Unauthorized` | Authentication failed.                                                                                                                    |

Both `200` and `201` responses share the shape `{ "data": { /* persisted event */ } }`. The persisted row includes a server-controlled `serverMetadata` object alongside the caller-supplied `metadata`. `serverMetadata.authMethod` reflects the authentication strategy that produced the request; this field cannot be set or overridden by the caller (the request schema does not accept `serverMetadata`).

## Idempotency

Pass a stable `id` value on retries. If a row with that `(tenantId, projectId, id)` triple already exists, the server returns `200` with the original row — the original `createdAt` is preserved and the event is not modified. This makes browser-side fire-and-forget calls safe to retry.

## User attribution

`userProperties` is a free-form JSON object — the framework accepts arbitrary keys with arbitrary values and stores the object as-is. The fields below are **recommended conventions** for cross-product consistency, not server-validated shapes:

| Field              | Recommended type | Notes                                               |
| ------------------ | ---------------- | --------------------------------------------------- |
| `id`               | string \| number | Caller-supplied widget identifier.                  |
| `userId`           | string \| number | The end-user identifier.                            |
| `supportAgentName` | string           | If a support agent is acting on behalf of the user. |
| `userType`         | string           | Caller-defined user type.                           |
| (extras)           | any              | Any other keys are stored as-is.                    |

The server does **not** validate field types within `userProperties` — `{ userId: 123 }` and `{ userId: "u_abc" }` are both accepted. Customers should pick a convention and stick to it within their own data.

`userProperties` is **caller-trusted analytics attribution metadata**. Caller-supplied values always win on every authentication path — the server never overrides them. When `userProperties` is absent from the request body, the server resolves it from the message anchor (when `messageId` is supplied), then the conversation anchor (when `conversationId` is supplied or derivable from the message), and finally falls back to `null`. The server does not merge individual fields — `userProperties` is treated as an opaque object — and does not auto-fill from the authenticated execution context. For server-verified user identity, read `conversation.userId` (the cryptographically verified `sub` claim from JWT auth, populated on every chat turn).

The chat-handler write path (the indirect source of conversation/message anchors) drops widget-synthesized auto-mint identities (`identificationType` of `'ANONYMOUS'` or `'COOKIED'`) and strips the `identificationType` marker from anything else. This keeps placeholder anonymous identities from polluting webhook payloads. Direct `POST /run/v1/events` callers receive the same defense — auto-mint shapes in the request body are filtered the same way before persistence.

For server-authoritative auth audit (which authentication strategy was used), see `serverMetadata.authMethod` — that field is server-controlled and immutable to callers.

PII content placed in `userProperties` is the caller's responsibility — the framework does not redact or allowlist field contents.

## Free-form events

The schema accepts events with no anchor (`conversationId`, `messageId`, and `agentId` are all independently optional). Receivers downstream of the events table define their own policy for non-anchored events.

## Forward-anchored events

`conversationId` and `messageId` are not foreign keys — events may reference an anchor ID for a row that does not yet exist in the `conversations` or `messages` table. This is the expected pattern for clients that mint a `conversationId` client-side and fire `user_message_submitted` before the chat handler creates the corresponding conversation row.

Webhook consumers that fetch the conversation on receipt may briefly observe a 404. Treat the event payload as the source of truth and retry the conversation fetch if needed.

Conversation deletion still removes any matching events — the framework cleans events up at the application layer when a conversation is deleted. Events anchored only by `messageId` (with `conversationId` left null) are reclaimed at project deletion rather than at conversation deletion. Events whose `conversationId` is never created (e.g. typos, never-completed flows) also persist until project deletion.

<Note>
  This API does not validate that `conversationId` or `messageId` reference existing rows. Events with unresolvable anchors are accepted with 201. To catch typos in anchor IDs, validate IDs client-side.
</Note>

## Event vocabulary

The framework does not ship an event-name registry. The `type` field is a free-form string and consumers define their own vocabulary. Two precedents to follow if you're authoring a consumer:

* **Inkeep widget chat events** are typed by `@inkeep/agents-ui`'s `InkeepEvent` discriminated union (chat, search, and widget event categories). When the widget self-instruments lifecycle events in a future release, those names are the canonical widget vocabulary.
* **Consumer-specific event names** (action events, generation events, custom product events) live in the consumer's own package as a `const` + derived union pattern. They flow through the API as plain strings.

Webhook destinations subscribe to event families (`conversation events`, `feedback events`, `event.created`), not to specific `type` values. Receivers filter on `type` after delivery — this keeps the framework decoupled from any particular event vocabulary.

## Outbound webhook delivery

After a successful insert, the route dispatches the persisted event to every webhook destination subscribed to the `event.created` family for the project (and matching the resolved agent scope). Delivery is fire-and-forget — the existing webhook delivery infrastructure handles retries, signing, and SSRF protection. Subscribe a destination by checking the **Event Created** option on the webhook destination form.

Webhook dispatch requires both an **agent scope** (`agentId` either supplied in the request body or inferred from the API key's authenticated context) and a **resolved ref** on the request. Free-form events without an `agentId` — or events authenticated without a resolvable agent ref — are still **persisted** to the events table, but no webhook fires for them. Operators can confirm via the `'Skipping event.created webhook dispatch'` debug log on the run-events logger when this code path is taken.


# MCP Server (/talk-to-your-agents/mcp-server)

Learn how to use the MCP server to talk to your agents



The MCP server allows you to talk to your agents through the Model Context Protocol.

<>
  ## Authentication

  Choose the authentication method:

  <Tabs>
    <Tab title="App Credential (app-based)">
      App credentials are the auth method for app-based integrations such as web clients, support copilots, and authenticated end-user experiences. For web clients, the App ID is safe to expose client-side.

      **Setup:**

      1. [Create an app credential](/talk-to-your-agents/app-credentials#create-an-app-via-api) for your project
      2. Request an anonymous session token from your client
      3. Use the token and App ID in chat requests

      **Request Headers:**

      ```http
      Authorization: Bearer <session_token>
      X-Inkeep-App-Id: <app_id>
      ```

      See [App Credentials](/talk-to-your-agents/app-credentials) for the full auth flow details.
    </Tab>

    <Tab title="API Key (server-to-server)">
      Use API keys for server-to-server integrations where the secret is never exposed to end-users.

      **Create an API Key:**

      1. Open the Visual Builder Dashboard
      2. Go to your Project → **API Keys**
      3. Click **Create**, select the target agent
      4. Copy the API key (it will be shown once) and store it securely

      **Request Header:**

      ```http
      Authorization: Bearer <api_key>
      ```

      <Warning>
        API keys are secrets. Never expose them in client-side code or browser requests.
      </Warning>
    </Tab>

    <Tab title="Development (no auth)">
      When running the API server locally with `pnpm dev`, authentication is automatically bypassed. You can use headers in the request instead:

      **Request Headers:**

      ```http
      x-inkeep-tenant-id: <tenant-id>
      x-inkeep-project-id: <project-id>
      x-inkeep-agent-id: <agent-id>
      ```

      <Warning>
        This mode is for development only. Never use in production as it bypasses all security checks.
      </Warning>
    </Tab>
  </Tabs>

  See [Authentication → Run API](/api-reference/authentication/run-api) for more details.
</>

## MCP Server Implementation

The MCP server is implemented in the `@inkeep/agents-api` library and provides a standard interface for agent communication.

## Available Tools

The MCP server exposes one core tool:

### send-query-to-agent

Sends a query to your agent's default sub-agent. This tool:

* **Name**: `send-query-to-agent`
* **Description**: Dynamically generated based on your agent's name and description
* **Parameters**:
  * `query` (string): The query to send to the agent
* **Returns**: The agent's response as text content

**Example usage in Cursor:**
When the MCP server is configured, Cursor will automatically discover this tool and you can use it by asking questions. The tool will route your query to the appropriate sub-agent in your agent and return the response.

The tool handles:

* Message creation and conversation management
* Sub-agent execution with your configured tools and capabilities
* Context resolution if your agent has context configuration
* Error handling and response formatting

## Using with Cursor

### Quick Install (Inkeep Hosted Docs MCP)

Install the Inkeep Agents documentation MCP server with one click:

<div
  style={{
  display: "flex",
  gap: "10px",
  marginBottom: "16px",
  flexWrap: "wrap",
}}
>
  <a href="cursor://anysphere.cursor-deeplink/mcp/install?name=inkeep-agents&config=eyJ1cmwiOiJodHRwczovL2FnZW50cy5pbmtlZXAuY29tL21jcCJ9">
    <img src="https://cursor.com/deeplink/mcp-install-dark.png" alt="Add to Cursor" style={{ height: "40px" }} />
  </a>

  <a href="vscode:mcp/install?%7B%22name%22%3A%22inkeep-agents%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Fagents.inkeep.com%2Fmcp%22%7D">
    <img src="https://img.shields.io/badge/VS_Code-Install_Inkeep_Agents-0098FF?style=flat-square&logo=visualstudiocode&logoColor=ffffff" alt="Install in VS Code" />
  </a>
</div>

### Manual Configuration

Add the following configuration to your Cursor MCP settings.

#### Example

Example when using an API key for auth:

```json
{
  "AgentName": {
    "type": "mcp",
    "url": "http://<your-agents-api-url>/run/v1/mcp",
    "headers": {
      "Authorization": "Bearer <agent_api_key>"
    }
  }
}
```

## Session Management (Required by MCP HTTP Transport)

* Initialize a session by sending an `initialize` JSON-RPC request to `/run/v1/mcp`.
* The server will respond and set `Mcp-Session-Id` in response headers.
* For all subsequent JSON-RPC requests in that session, include `Mcp-Session-Id` header with the value from initialization.

<Note>
  Session management is required by MCP’s HTTP transport. If `Mcp-Session-Id` is
  missing or invalid on follow-up requests, the server will return a JSON-RPC
  error (e.g., "Session not found").
</Note>

## Configuration Notes

* **URL**: Point to your `agents-api` instance (default: `http://localhost:3002`)
* **Headers**: Use the appropriate authentication mode per the section above
* **Authorization**: Only required outside development mode


# Talk to Your Agents Overview (/talk-to-your-agents/overview)

Learn how to talk to your agents



<>
  You can talk to an Inkeep Agent in a few ways, including:

  * **UI Chat Components**: Drop-in React components for chat UIs with built-in streaming and rich UI customization. See [`agents-ui`](/talk-to-your-agents/react/chat-button).
  * **As an MCP server**: Use your Inkeep Agent as if was an MCP Server. Allows you to connect it to any MCP client, like Claude, ChatGPT, Claude and other Agents. See [MCP server](/talk-to-your-agents/mcp-server).
  * **Via API (Vercel format)**: An API that streams responses over server-side events (SSE). Use from any language/runtime, including the Vercel's `useChat` and AI Element primitives for custom UIs. See [API (Vercel format)](/talk-to-your-agents/api).
  * **Via API (A2A format)**: An API that follows the Agent-to-Agent ('A2A') JSON-RPC protocol. Great for when combining Inkeep with different Agent frameworks that support the A2A format. See [A2A protocol](/talk-to-your-agents/a2a).
  * **Via Webhook Triggers**: Create webhook endpoints that allow external services (GitHub, Slack, Stripe, etc.) to invoke your Agents. See [Triggers](/talk-to-your-agents/triggers).
  * **Slack**: Interact with agents directly in Slack using `@Inkeep` and slash commands. Available for Enterprise. See [Slack](/talk-to-your-agents/slack/overview).

  <Cards>
    <Card title="React UI" icon="LuMessageSquare" href="/talk-to-your-agents/react/chat-button">
      Drop-in chat components for React apps with streaming and rich UI.
    </Card>

    <Card title="API (Vercel format)" icon="LuNetwork" href="/talk-to-your-agents/chat-api">
      POST /api/chat, SSE (text/event-stream), x-vercel-ai-data-stream: v2.
    </Card>

    <Card title="A2A protocol" icon="LuNetwork" href="/talk-to-your-agents/a2a">
      JSON-RPC messages at /agents/a2a with blocking and streaming modes.
    </Card>

    <Card title="MCP server" icon="LuServer" href="/talk-to-your-agents/mcp-server">
      HTTP JSON-RPC endpoint at /v1/mcp with session header management.
    </Card>

    <Card title="Triggers" icon="LuZap" href="/talk-to-your-agents/triggers">
      Webhook and scheduled triggers for event-driven Agent invocation.
    </Card>

    <Card title="Slack" icon="LuMessageSquare" href="/talk-to-your-agents/slack/overview">
      Mention `@Inkeep` or use `/inkeep` commands to talk to agents in Slack.
    </Card>
  </Cards>
</>


# Triggers (/talk-to-your-agents/triggers)

Invoke your Agents automatically via webhooks or scheduled executions



Triggers let external services and schedules invoke your Agents without manual interaction. There are two types:

* **Webhook triggers** — Create HTTP endpoints that external services (GitHub, Slack, Stripe, Zendesk, etc.) can call to start a conversation with your Agent. When a webhook is received, the payload is validated, transformed, and passed to the Agent asynchronously.
* **Scheduled triggers** — Run Agents on a recurring cron schedule or at a specific future time. Use them for daily reports, hourly health checks, periodic data syncs, or deferred tasks.

## Configure triggers

You can configure triggers either in the dashboard or programmatically in the TypeScript SDK.

<Cards>
  <Card title="Visual Builder" icon="LuLayoutDashboard" href="/visual-builder/triggers/webhooks">
    Set up webhook and scheduled triggers from the dashboard.
  </Card>

  <Card title="TypeScript SDK" icon="LuCode" href="/typescript-sdk/triggers/overview">
    Define triggers programmatically in your agent configuration.
  </Card>
</Cards>


# Sub Agent Relationships (/typescript-sdk/agent-relationships)

Learn how to add Sub Agent relationships to your agent



<SkillRule id="agent-relationships-overview" skills="typescript-sdk" title="Sub Agent Relationships Overview" description="Transfer and delegation relationships between Sub Agents">
  Sub Agent relationships are used to coordinate specialized Sub Agents for complex workflows. This framework implements Sub Agent relationships through using `canDelegateTo()` and `canTransferTo()`, allowing a parent Sub Agent to automatically coordinate specialized Sub Agents for complex workflows.

  ## Understanding Sub Agent Relationships

  The framework supports two types of Sub Agent relationships:

  ### Transfer Relationships

  Transfer relationships **completely relinquish control** from one Sub Agent to another. When a Sub Agent hands off to another:

  * The source Sub Agent stops processing
  * The target Sub Agent takes full control of the conversation
  * Control is permanently transferred until the target Sub Agent hands back

  ```typescript
  import { subAgent, agent } from "agent-sdk";

  // Create specialized Sub Agents first
  const qaSubAgent = subAgent({
    id: "qa-agent",
    name: "QA Sub Agent",
    description: "Answers product and service questions",
    prompt: "Provide accurate information using available tools. Hand back to router if unable to help.",
    canUse: () => [knowledgeBaseTool],
    canTransferTo: () => [routerSubAgent],
  });

  const orderSubAgent = subAgent({
    id: "order-agent",
    name: "Order Sub Agent",
    description: "Handles order-related inquiries and actions",
    prompt: "Assist with order tracking, modifications, and management.",
    canUse: () => [orderSystemTool],
    canTransferTo: () => [routerSubAgent],
  });

  // Create router Sub Agent that coordinates other Sub Agents
  const routerSubAgent = subAgent({
    id: "router-agent",
    name: "Router Sub Agent",
    description: "Routes customer inquiries to specialized Sub Agents",
    prompt: `Analyze customer inquiries and route them appropriately:
      - Product questions → Hand off to QA Sub Agent
      - Order issues → Hand off to Order Sub Agent
      - Complex issues → Handle directly or escalate`,
    canTransferTo: () => [qaSubAgent, orderSubAgent],
  });

  // Create the agent with router as default entry point
  const supportAgent = agent({
    id: "customer-support-agent",
    defaultSubAgent: routerSubAgent,
    subAgents: () => [routerSubAgent, qaSubAgent, orderSubAgent],
    models: {
      base: {
        model: "anthropic/claude-sonnet-4-5",
        providerOptions: {
          temperature: 0.5,
        },
      },
      structuredOutput: {
        model: "openai/gpt-4.1-mini",
      },
    },
  });
  ```

  ### Delegation Relationships

  Delegation relationships are used to **pass a task** from one Sub Agent to another while maintaining oversight:

  * The source Sub Agent remains in control
  * The target Sub Agent executes a specific task
  * Results are returned to the source Sub Agent
  * The source Sub Agent continues processing
  * If a user redirects the task mid-delegation (e.g. by denying a tool call with a reason like "I want Tokyo instead"), the delegation response includes a note about the redirect so the parent Sub Agent has full context about how the task evolved

  ```typescript
  // Sub Agents for specific tasks
  const numberProducerA = subAgent({
    id: "number-producer-a",
    name: "Number Producer A",
    description: "Produces low-range numbers (0-50)",
    prompt: "Generate numbers between 0 and 50. Respond with a single integer.",
  });

  const numberProducerB = subAgent({
    id: "number-producer-b",
    name: "Number Producer B",
    description: "Produces high-range numbers (50-100)",
    prompt: "Generate numbers between 50 and 100. Respond with a single integer.",
  });

  // Coordinating Sub Agent that delegates tasks
  const mathSupervisor = subAgent({
    id: "math-supervisor",
    name: "Math Supervisor",
    description: "Coordinates mathematical operations",
    prompt: `When given a math task:
      1. Delegate to Number Producer A for a low number
      2. Delegate to Number Producer B for a high number
      3. Add the results together and provide the final answer`,
    canDelegateTo: () => [numberProducerA, numberProducerB],
  });

  const mathAgent = agent({
    id: "math-delegation-agent",
    defaultSubAgent: mathSupervisor,
    subAgents: () => [mathSupervisor, numberProducerA, numberProducerB],
    models: {
      base: {
        model: "anthropic/claude-haiku-4-5",
        providerOptions: {
          temperature: 0.1,
        }
      }
    },
  });
  ```
</SkillRule>

<SkillRule id="agent-relationships-when-to-use" skills="typescript-sdk" title="When to Use Transfers vs Delegation" description="Guidelines for choosing between transfer and delegation relationships">
  ## When to Use Each Relationship

  ### Use Transfers for Complex Tasks

  Use `canTransferTo` when the task is complex and the user will likely want to ask follow-up questions to the specialized Sub Agent:

  * **Customer support conversations** - User may have multiple related questions
  * **Technical troubleshooting** - Requires back-and-forth interaction
  * **Order management** - User might want to modify, track, or ask about multiple aspects
  * **Product consultations** - Users often have follow-up questions

  ### Use Delegation for Simple Tasks

  Use `canDelegateTo` when the task is simple and self-contained:

  * **Data retrieval** - Get a specific piece of information and return it
  * **Calculations** - Perform a computation and return the result
  * **Single API calls** - Make one external request and return the data
  * **Simple transformations** - Convert data from one format to another

  ```typescript
  // TRANSFER: User will likely have follow-up questions about their order
  const routerSubAgent = subAgent({
    id: "router",
    prompt: "For order inquiries, transfer to order specialist",
    canTransferTo: () => [orderSubAgent],
  });

  // DELEGATION: Just need a quick calculation, then continue
  const mathSupervisor = subAgent({
    id: "supervisor",
    prompt: "Delegate to number producers, then add results together",
    canDelegateTo: () => [numberProducerA, numberProducerB],
  });
  ```
</SkillRule>

<SkillRule id="delegation-types" skills="typescript-sdk" title="Types of Delegation Relationships" description="Sub agent, external agent, and team agent delegation">
  ## Types of Delegation Relationships

  ### Sub Agent Delegation

  Sub agent delegation is used to delegate a task to a sub agent as seen above.

  ### External Agent Delegation

  External agent delegation is used to delegate a task to an [external agent](/typescript-sdk/external-agents).

  ```typescript
  import { myExternalAgent } from "./external-agents/external-agent-example";

  const mathSupervisor = subAgent({
    id: "supervisor",
    prompt: "Delegate to the external agent to calculate the answer to the question",
    canDelegateTo: () => [myExternalAgent],
  });
  ```

  You can also specify headers to include with every request to the external agent.

  ```typescript
  const mathSupervisor = subAgent({
    id: "supervisor",
    prompt: "Delegate to the external agent to calculate the answer to the question",
    canDelegateTo: () => [myExternalAgent.with({ headers: { "authorization": "my-api-key" } })],
  });
  ```

  ### Team Agent Delegation

  Team agent delegation is used to delegate a task to another agent in the same project.

  ```typescript
  import { myAgent } from "./agents/my-team-agent";

  const mathSupervisor = subAgent({
    id: "supervisor",
    prompt: "Delegate to the team agent to calculate the answer to the question",
    canDelegateTo: () => [myAgent],
  });
  ```

  You can also specify headers to include with every request to the team agent.

  ```typescript
  const mathSupervisor = subAgent({
    id: "supervisor",
    prompt: "Delegate to the team agent to calculate the answer to the question",
    canDelegateTo: () => [myAgent.with({ headers: { "authorization": "my-api-key" } })],
  });
  ```

  ## Requiring a transfer

  By default a sub-agent that declares `canTransferTo` *may* transfer. To require that a sub-agent hands off on every turn, see the [Output Contract](/typescript-sdk/structured-outputs/output-contract).
</SkillRule>


# Agents & Sub Agents (/typescript-sdk/agent-settings)

Learn how to customize your Agents.



Agents and Sub Agents are the core building blocks of the Inkeep Agent framework.

An Agent is made up of one or more Sub Agents that can delegate or transfer control with each other, share context, use tools to respond to a user or complete a task.

## Creating an Agent

An Agent is your top-level entity that you as a user interact with or can trigger programmatically.

An Agent is made up of sub-agents, like so:

```typescript
// Agent-level prompt that gets added to all Sub Agents
const customerSupportAgent = agent({
  id: "support-agent",
  prompt: `You work for Acme Corp. Always be professional and helpful.`,
  subAgents: () => [supportAgent, escalationAgent],
});
```

**The `prompt` is automatically put into context and added into each Sub Agent's system prompt.** This provides consistent behavior and tone to all Sub Agents so they can act and respond as one cohesive unit to the end-user.

## Creating a Sub Agent

Like an Agent, a Sub Agent needs an id, name, and clear prompt that define its behavior:

```typescript
import { subAgent } from "@inkeep/agents-sdk";

const supportAgent = subAgent({
  id: "customer-support",
  name: "Customer Support Agent",
  prompt: `You are a customer support specialist.`,
});
```

## Configuring Models

Configure AI models for your agents. See [Model Configuration](/typescript-sdk/models) for detailed information about supported providers, configuration options, and examples.

<SkillRule id="stop-when-config" skills="typescript-sdk" title="Configuring StopWhen" description="Control stopping conditions to prevent infinite loops in agents">
  ## Configuring StopWhen

  Control stopping conditions to prevent infinite loops:

  ```typescript
  // Agent level - limit transfers between Sub Agents
  agent({
    id: "support-agent",
    stopWhen: {
      transferCountIs: 5  // Max transfers in one conversation
    },
  });

  // Sub Agent level - limit generation steps
  subAgent({
    id: "my-sub-agent",
    stopWhen: {
      stepCountIs: 20  // Max tool calls + LLM responses
    },
  });
  ```

  **Configuration levels:**

  * `transferCountIs`: Project or Agent level
  * `stepCountIs`: Project or Sub Agent level

  Settings inherit from Project → Agent → Sub Agent.
</SkillRule>

<SkillRule id="sub-agent-params" skills="typescript-sdk" title="Sub Agent Parameters Reference" description="Complete parameter reference for subAgent() configuration">
  ## Sub Agent overview

  Beyond model configuration, Sub Agents define tools, structured outputs, and agent-to-agent relationships available to the Sub Agent.

  | Parameter            | Type     | Required | Description                                                                                                                                                         |
  | -------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `id`                 | string   | Yes      | Stable Sub Agent identifier used for consistency and persistence                                                                                                    |
  | `name`               | string   | Yes      | Human-readable name for the Sub Agent                                                                                                                               |
  | `prompt`             | string   | Yes      | Detailed behavior guidelines and system prompt for the Sub Agent                                                                                                    |
  | `description`        | string   | No       | Brief description of the Sub Agent's purpose and capabilities                                                                                                       |
  | `models`             | object   | No       | Model configuration for this Sub Agent. See [Model Configuration](/typescript-sdk/models)                                                                           |
  | `stopWhen`           | object   | No       | Stop conditions (`stepCountIs`). See [Configuring StopWhen](#configuring-stopwhen)                                                                                  |
  | `canUse`             | function | No       | Returns the list of MCP/tools the Sub Agent can use. See [MCP Servers](/guides/mcp-servers/overview) for how to find or build MCP servers                           |
  | `dataComponents`     | array    | No       | Structured output components for rich, interactive responses. See [Data Components](/typescript-sdk/structured-outputs/data-components)                             |
  | `artifactComponents` | array    | No       | Components for handling tool or Sub Agent outputs. See [Artifact Components](/typescript-sdk/structured-outputs/artifact-components)                                |
  | `canTransferTo`      | function | No       | Function returning array of Sub Agents this Sub Agent can transfer to. See [Transfer Relationships](/typescript-sdk/agent-relationships#transfer-relationships)     |
  | `canDelegateTo`      | function | No       | Function returning array of Sub Agents this Sub Agent can delegate to. See [Delegation Relationships](/typescript-sdk/agent-relationships#delegation-relationships) |
</SkillRule>

### Tools & MCPs

Enable tools for a Sub Agent to perform actions like looking up information or calling external APIs.

Tools can be:

* **[MCP Servers](/typescript-sdk/tools/mcp-tools)** - Connect to external services and APIs using the Model Context Protocol
* **[Function Tools](/typescript-sdk/tools/function-tools)** - Custom JavaScript functions that execute directly in secure sandboxes

```typescript
import { subAgent, functionTool, mcpTool } from "@inkeep/agents-sdk";

const mySubAgent = subAgent({
  id: "my-agent-id",
  name: "My Sub Agent",
  prompt: "Detailed behavior guidelines",
  canUse: () => [
    functionTool({
      name: "get-current-time",
      description: "Get the current time",
      execute: async () => ({ time: new Date().toISOString() }),
    }),
    mcpTool({
      id: "inkeep-kb-rag",
      name: "Knowledge Base Search",
      description: "Search the company knowledge base.",
      serverUrl: "https://rag.inkeep.com/mcp",
    }),
  ],
});
```

### Data components

Structured output components for rich, interactive responses. See [Data Components](/typescript-sdk/structured-outputs/data-components).

```typescript
import { z } from 'zod';

const mySubAgent = subAgent({
  id: "my-agent-id",
  name: "My Sub Agent",
  prompt: "Detailed behavior guidelines",
  dataComponents: [
    {
      id: "customer-info",
      name: "CustomerInfo",
      description: "Customer information display component",
      props: z.object({
        name: z.string().describe("Customer name"),
        email: z.string().describe("Customer email"),
        issue: z.string().describe("Customer issue description"),
      }),
    },
  ],
});
```

### Artifact components

Components for handling tool or Sub Agent outputs. See [Artifact Components](/typescript-sdk/structured-outputs/artifact-components).

Use `preview()` to mark fields that should be **immediately available** in the agent's context — agents can reason about these fields directly. Fields without `preview()` are persisted in storage but kept out of context by default, keeping the context window lean. Agents can retrieve the complete artifact on demand when they need those fields.

```typescript
import { z } from 'zod';
import { preview } from '@inkeep/agents-core';

const mySubAgent = subAgent({
  id: "my-agent-id",
  name: "My Sub Agent",
  prompt: "Detailed behavior guidelines",
  artifactComponents: [
    {
      id: "customer-info",
      name: "CustomerInfo",
      description: "Customer information display component",
      props: z.object({
        name: preview(z.string().describe("Customer name")),  // available immediately in context
        customer_info: z.string().describe("Customer information"),  // available on demand
      }),
    },
  ],
});
```

### Sub Agent relationships

Define other Sub Agents this Sub Agent can transfer control to or delegate tasks to.

```typescript
const mySubAgent = subAgent({
  // ...
  canTransferTo: () => [subAgent1],
  canDelegateTo: () => [subAgent2],
});
```

As a next step, see [Sub Agent Relationships](/typescript-sdk/agent-relationships) to learn how to design transfer and delegation relationships between Sub Agents.


# CLI Reference (/typescript-sdk/cli-reference)

Complete reference for the Inkeep CLI commands



## Overview

The Inkeep CLI is the primary tool for interacting with the Inkeep Agent Framework. It allows you to push Agent configurations and interact with your multi-agent system.

## Installation

```bash
# Install the CLI globally
npm install -g @inkeep/agents-cli

# Install the dashboard package (for visual agents orchestration)
npm install @inkeep/agents-manage-ui
```

## Global Options

All commands support the following global options:

* `--version` - Display CLI version
* `--help` - Display help for a command

<SkillRule id="cli-init-command" skills="typescript-sdk" title="CLI inkeep init Command" description="Initialize a new Inkeep configuration file in your project">
  ## Commands

  ### `inkeep init`

  Initialize a new Inkeep configuration file in your project.

  ```bash
  inkeep init [path]
  ```

  **Options:**

  * `--local` - Initialize for local development (creates a local profile with no authentication required)
  * `--no-interactive` - Skip interactive path selection
  * `--config <path>` - Path to use as template for new configuration

  **Cloud vs Local Initialization:**

  By default, `inkeep init` runs the remote instance onboarding wizard which:

  * Opens browser-based authentication
  * Fetches your organizations and projects from remote instance
  * Creates project directories with configuration files
  * Sets up the `cloud` profile as the default in `~/.inkeep/profiles.yaml`

  Use `--local` for local development:

  * Prompts for tenant ID and API URL (defaults to `localhost:3002`); Manage UI defaults to `localhost:3000`
  * Creates `inkeep.config.ts` with your local configuration
  * Sets up a `local` profile as the default in `~/.inkeep/profiles.yaml`
  * No authentication required

  For self-hosted deployments that require authentication, use `inkeep profile add` and select **Custom**.

  **Examples:**

  ```bash
  # Cloud initialization (default) - opens browser for auth
  inkeep init

  # Local initialization - no auth required
  inkeep init --local

  # Initialize in specific directory
  inkeep init ./my-project

  # Local init in specific directory
  inkeep init --local ./my-project

  # Non-interactive mode
  inkeep init --no-interactive

  # Use specific config as template
  inkeep init --config ./template-config.ts
  ```
</SkillRule>

<SkillRule id="cli-push-command" skills="typescript-sdk" title="CLI inkeep push Command" description="Push agent configurations to your server">
  ### `inkeep push`

  **Primary use case:** Push a project containing Agent configurations to your server. This command deploys your entire multi-agent project, including all Agents, Sub Agents, and tools.

  ```bash
  inkeep push
  ```

  **Options:**

  * `--project <project-id>` - Project ID or path to project directory
  * `--all` - Push all projects found in the current directory tree
  * `--env <environment>` - Load environment-specific credentials from `environments/<environment>.env.ts`
  * `--config <path>` - Override config file path (bypasses automatic config discovery)
  * `--tenant-id <id>` - Override tenant ID
  * `--agents-api-url <url>` - Override the agents API URL from config
  * `--json` - Generate project data as JSON file instead of pushing to server
  * `--profile <name>` - Use a specific CLI profile (overrides the active profile)
  * `--quiet` - Suppress interactive prompts and extra logs
  * `--force` - Skip conflict detection and push directly to remote

  #### Single Project Push

  The most common workflow is to run `inkeep push` from inside a project directory. A project directory is identified by having an `index.ts` file that exports a project object (with `__type = "project"`).

  ```bash
  # Navigate to your project directory
  cd my-project

  # Push the current project
  inkeep push
  ```

  The CLI will:

  1. Detect `index.ts` in the current directory
  2. Load the project export from `index.ts`
  3. Search upward for `inkeep.config.ts` to get tenant/API configuration
  4. Push the project to the server

  **Examples:**

  ```bash
  # Push project from current directory (most common)
  cd my-project && inkeep push

  # Push specific project directory
  inkeep push --project ./my-project

  # Push all projects in current directory tree
  inkeep push --all

  # Push with development environment credentials
  inkeep push --env development

  # Generate project JSON without pushing
  inkeep push --json

  # Override tenant ID
  inkeep push --tenant-id my-tenant

  # Override API URL
  inkeep push --agents-api-url https://api.example.com

  # Use specific config file
  inkeep push --config ./custom-config/inkeep.config.ts

  # Use a named profile (overrides the active profile)
  inkeep push --profile staging

  # Quiet non-essential output for CI
  inkeep push --quiet

  # Force push, skipping conflict detection
  inkeep push --force
  ```

  #### Batch Push with `--all`

  The `--all` flag discovers and pushes all projects in the current directory tree. A directory is considered a project if its `index.ts` file exports an object with `__type = "project"`.

  ```bash
  # From a workspace root containing multiple projects
  inkeep push --all
  ```

  **Project Discovery:**

  1. Searches recursively for directories containing `index.ts` files
  2. Validates each `index.ts` exports a project (checks for `__type = "project"`)
  3. Ensures each project can access an `inkeep.config.ts` (in the same directory or a parent directory)

  **Shared Configuration:**

  You can use a single `inkeep.config.ts` at the workspace root for multiple projects:

  ```
  workspace-root/
  ├── inkeep.config.ts          # Shared config for all projects
  └── projects/
      ├── project-a/
      │   └── index.ts          # Project A
      ├── project-b/
      │   └── index.ts          # Project B
      └── project-c/
          └── index.ts          # Project C
  ```

  Running `inkeep push --all` from `workspace-root/` or `workspace-root/projects/` will discover and push all three projects using the shared configuration.

  **Output:**

  ```
  🚀 Batch Push: Finding all projects...

  Found 3 project(s) to push:

    • project-a
    • project-b
    • project-c

  [1/3] Pushing project-a...
    ✓ Project A
  [2/3] Pushing project-b...
    ✓ Project B
  [3/3] Pushing project-c...
    ✓ Project C

  📊 Batch Push Summary:
    ✓ Succeeded: 3
  ```

  **Environment Credentials:**

  The `--env` flag loads environment-specific credentials when pushing your project. This will look for files like `environments/development.env.ts` or `environments/production.env.ts` in your project directory and load the credential configurations defined there.

  **Example environment file:**

  ```typescript
  // environments/development.env.ts
  import { CredentialStoreType } from "@inkeep/agents-core";
  import { registerEnvironmentSettings } from "@inkeep/agents-sdk";

  export const development = registerEnvironmentSettings({
    credentials: {
      "api-key-dev": {
        id: "api-key-dev",
        type: CredentialStoreType.memory,
        credentialStoreId: "memory-default",
        retrievalParams: {
          key: "API_KEY_DEV",
        },
      },
    },
  });
  ```

  #### Project Discovery and Structure

  The `inkeep push` command follows this discovery process:

  1. **Config File Discovery**: Searches for `inkeep.config.ts` using this pattern:

     * Starts from current working directory
     * Traverses **upward** through parent directories until found
     * Can be overridden by providing a path to the config file with the `--config` flag

  2. **Workspace Structure**: Expects this directory layout:

     ```
     workspace-root/
     ├── package.json              # Workspace package.json
     ├── tsconfig.json             # Workspace TypeScript config
     ├── inkeep.config.ts          # Inkeep configuration file
     ├── my-project/               # Individual project directory
     │   ├── index.ts              # Project entry point
     │   ├── agents/               # Agent definitions
     │   │   └── *.ts
     │   ├── tools/                # Tool definitions
     │   │   └── *.ts
     │   ├── data-components/      # Data component definitions
     │   │   └── *.ts
     │   └── environments/         # Environment-specific configs
     │       ├── development.env.ts
     │       └── production.env.ts
     └── another-project/          # Additional projects
         └── index.ts
     ```

  3. **Resource Compilation**: Automatically discovers and compiles:
     * All project directories containing `index.ts`
     * All TypeScript files within each project directory
     * Categorizes files by type (agents, Sub Agents, tools, data components)
     * Resolves dependencies and relationships within each project

  #### Push Behavior

  When pushing, the CLI:

  * Finds and loads configuration from `inkeep.config.ts` at workspace root
  * Discovers all project directories containing `index.ts`
  * Applies environment-specific settings if `--env` is specified
  * Compiles all project resources defined in each project's `index.ts`
  * Validates Sub Agent relationships and tool configurations across all projects
  * Deploys all projects to the management API
  * Prints deployment summary with resource counts per project
</SkillRule>

<SkillRule id="cli-pull-command" skills="typescript-sdk" title="CLI inkeep pull Command" description="Pull project configuration from server and update local TypeScript files">
  ### `inkeep pull`

  Pull project configuration from the server and update all TypeScript files in your local project using LLM generation.

  ```bash
  inkeep pull
  ```

  **Options:**

  * `--project <project-id>` - Project ID to pull (or path to project directory). If in project directory, validates against local project ID.
  * `--all` - Pull all projects from the server to the current directory
  * `--config <path>` - Path to configuration file
  * `--env <environment>` - Environment file to generate (development, staging, production). Defaults to development
  * `--json` - Output project data as JSON instead of generating files
  * `--debug` - Enable debug logging
  * `--verbose` - Enable verbose logging
  * `--force` - Force regeneration even if no changes detected
  * `--introspect` - Completely regenerate all files from scratch (no comparison needed)
  * `--profile <name>` - Use a specific CLI profile (overrides the active profile)
  * `--quiet` - Suppress interactive prompts and extra logs
  * `--conflict-strategy <strategy>` - Auto-resolve merge conflicts: ours (keep local) or theirs (accept remote). Skips interactive prompts.

  #### Single Project Pull

  The most common workflow is to run `inkeep pull` from inside a project directory. A project directory is identified by having an `index.ts` file that exports a project object (with `__type = "project"`).

  ```bash
  # Navigate to your project directory
  cd my-project

  # Pull updates from the server
  inkeep pull
  ```

  The CLI will:

  1. Detect `index.ts` in the current directory
  2. Load the project export to get the project ID
  3. Fetch the latest configuration from the server
  4. Update your local TypeScript files with any changes

  **Smart Project Detection:**

  The pull command intelligently handles different scenarios:

  1. **Project Directory Detection**: If your current directory contains an `index.ts` file that exports a project, the command automatically uses that project's ID
  2. **Project ID Validation**: If you specify `--project` while in a project directory, it validates that the ID matches your local project
  3. **Subdirectory Creation**: When not in a project directory, creates a new subdirectory named after the project ID

  **`--project` Flag Behavior:**

  The `--project` flag has dual functionality:

  * **Directory Path**: If the value points to a directory containing `index.ts`, it uses that directory
  * **Project ID**: If the value doesn't match a valid directory path, it treats it as a project ID

  **Pull Modes:**

  | Scenario                 | Command                                | Behavior                                                         |
  | ------------------------ | -------------------------------------- | ---------------------------------------------------------------- |
  | In project directory     | `inkeep pull`                          | ✅ Automatically detects project, pulls to current directory      |
  | In project directory     | `inkeep pull --project <matching-id>`  | ✅ Validates ID matches local project, pulls to current directory |
  | In project directory     | `inkeep pull --project <different-id>` | ❌ Error: Project ID doesn't match local project                  |
  | Not in project directory | `inkeep pull`                          | ❌ Error: Requires --project flag                                 |
  | Not in project directory | `inkeep pull --project <id>`           | ✅ Creates `<id>/` subdirectory with project files                |

  **How it Works:**

  The pull command discovers and updates all TypeScript files in your project based on the latest configuration from the server:

  1. **File Discovery**: Recursively finds all `.ts` files in your project (excluding `environments/` and `node_modules/`)
  2. **Smart Categorization**: Categorizes files as index, agents, Sub Agents, tools, or other files
  3. **Context-Aware Updates**: Updates each file with relevant context from the server:
     * **Agent files**: Updated with specific agent data
     * **Sub Agent files**: Updated with specific Sub Agent configurations
     * **Tool files**: Updated with specific tool definitions
     * **Other files**: Updated with full project context
  4. **LLM Generation**: Uses AI to maintain code structure while updating with latest data

  #### TypeScript Updates (Default)

  By default, the pull command updates your existing TypeScript files using LLM generation:

  1. **Context Preservation**: Maintains your existing code structure and patterns
  2. **Selective Updates**: Only updates relevant parts based on server configuration changes
  3. **File-Specific Context**: Each file type receives appropriate context (Agents get Agent data, Sub Agents get Sub Agent data, etc.)

  **Examples:**

  ```bash
  # Directory-aware pull: Automatically detects project from current directory
  cd my-project  # Directory contains index.ts with project export
  inkeep pull    # Pulls to current directory, no subdirectory created

  # Validate project ID while in project directory
  cd my-project  # Directory contains index.ts
  inkeep pull --project my-project  # Validates ID matches, pulls to current directory

  # Error case: Wrong project ID in project directory
  cd my-project  # Directory contains index.ts with different project ID
  inkeep pull --project different-project  # ERROR: Project ID mismatch

  # Pull when NOT in a project directory (requires --project)
  cd ~/projects
  inkeep pull  # ERROR: Requires --project flag

  # Pull specific project to new subdirectory
  cd ~/projects
  inkeep pull --project my-project  # Creates ./my-project/ subdirectory

  # Save project data as JSON file instead of updating files
  inkeep pull --json

  # Enable debug logging
  inkeep pull --debug

  # Enable verbose logging
  inkeep pull --verbose

  # Force regeneration even if no changes detected
  inkeep pull --force

  # Completely regenerate all files from scratch
  inkeep pull --introspect

  # Generate environment-specific credentials
  inkeep pull --env production

  # Use specific config file
  inkeep pull --config ./custom-config/inkeep.config.ts

  # Pull all projects from server
  inkeep pull --all

  # Pull using a specific profile (overrides active)
  inkeep pull --profile staging

  # Quiet non-essential output for CI
  inkeep pull --quiet
  ```

  #### Batch Pull with `--all`

  The `--all` flag fetches all projects from your tenant and pulls them to the current directory. Each project is created as a subdirectory.

  ```bash
  # From a workspace directory
  inkeep pull --all
  ```

  **How it Works:**

  1. Fetches all projects from the API for your tenant
  2. Categorizes each project as "existing" or "new":
     * **Existing**: Directory with `index.ts` already exists → uses smart comparison mode
     * **New**: No local directory → uses introspect mode for fresh generation
  3. Processes each project sequentially

  **Output:**

  ```
  🔄 Batch Pull: Sequential processing with smart comparison

    • Existing projects: Smart comparison + AST merging + confirmation prompts
    • New projects: Fresh generation with introspect mode

  Projects to pull:

    Existing (smart comparison):
      • My Agent (my-agent)
    New (introspect):
      • New Project (new-project)

  ──────────────────────────────────────────────────────────
  [1/2] Pulling My Agent...
    ✓ My Agent → ./my-agent

  ──────────────────────────────────────────────────────────
  [2/2] Pulling New Project...
    ✓ New Project → ./new-project

  ════════════════════════════════════════════════════════════
  📊 Batch Pull Summary:
    ✓ Succeeded: 2
  ```

  #### Model Configuration

  The `inkeep pull` command automatically selects the best available model for LLM generation based on your API keys:

  1. **Anthropic Claude** (if `ANTHROPIC_API_KEY` is set): `claude-sonnet-4-5`
  2. **OpenAI GPT** (if `OPENAI_API_KEY` is set): `gpt-5.1`
  3. **Google Gemini** (if `GOOGLE_GENERATIVE_AI_API_KEY` is set): `gemini-2.5-flash`

  The models are used for intelligent content merging when updating modified components, ensuring your local customizations are preserved while incorporating upstream changes.

  #### Validation Process

  The `inkeep pull` command includes a two-stage validation process to ensure generated TypeScript files accurately represent your backend configuration:

  **1. Basic File Verification**

  * Checks that all expected files exist (index.ts, agent files, tool files, component files)
  * Verifies file naming conventions match (kebab-case)
  * Ensures project export is present in index.ts

  **2. Round-Trip Validation** *(New in v0.24.0)*

  * Loads the generated TypeScript using the same logic as `inkeep push`
  * Serializes it back to JSON format
  * Compares the serialized JSON with the original backend data
  * Reports any differences found

  This round-trip validation ensures:

  * ✅ Generated TypeScript can be successfully loaded and imported
  * ✅ The serialization logic (TS → JSON) works correctly
  * ✅ Generated files will work with `inkeep push` without errors
  * ✅ No data loss or corruption during the pull process

  **Validation Output:**

  ```bash
  ✓ Basic file verification passed
  ✓ Round-trip validation passed - generated TS matches backend data
  ```

  **If validation fails:**

  The CLI will display specific differences between the generated and expected data:

  ```bash
  ✗ Round-trip validation failed

  ❌ Round-trip validation errors:
     The generated TypeScript does not serialize back to match the original backend data.
    • Value mismatch at agents.my-agent.name: "Original Name" vs "Generated Name"
    • Missing tool in generated: tool-id

  ⚠️  This indicates an issue with LLM generation or schema mappings.
  The generated files may not work correctly with `inkeep push`.
  ```

  **TypeScript generation fails:**

  * Ensure your network connectivity and API endpoints are correct
  * Check that your model provider credentials (if required by backend) are set up
  * Try using `--json` flag as a fallback to get the raw project data

  **Validation errors during pull:**

  * The generated TypeScript may have syntax errors or missing dependencies
  * Check the generated file manually for obvious issues
  * Try pulling as JSON first to verify the source data: `inkeep pull --json`
  * If round-trip validation fails, report the issue with the error details
</SkillRule>

<SkillRule id="cli-list-agents-command" skills="typescript-sdk" title="CLI inkeep list-agent Command" description="List all available agents for a specific project">
  ### `inkeep list-agent`

  List all available agents for a specific project.

  ```bash
  inkeep list-agent --project <project-id>
  ```

  **Options:**

  * `--project <project-id>` - **Required.** Project ID to list agents for
  * `--tenant-id <tenant-id>` - Tenant ID
  * `--agents-api-url <url>` - Agents API URL
  * `--config <path>` - Path to configuration file
  * `--config-file-path <path>` - Path to configuration file (deprecated, use --config)

  **Examples:**

  ```bash
  # List agents for a specific project
  inkeep list-agent --project my-project

  # List agents using a specific config file
  inkeep list-agent --project my-project --config ./inkeep.config.ts

  # Override tenant and API URL
  inkeep list-agent --project my-project --tenant-id my-tenant --agents-api-url https://api.example.com
  ```
</SkillRule>

<SkillRule id="cli-dev-command" skills="typescript-sdk" title="CLI inkeep dev Command" description="Start the Inkeep dashboard server for visual agents orchestration">
  ### `inkeep dev`

  Start the Inkeep dashboard server, build for production, or export the Next.js project.

  > **Note:** This command requires `@inkeep/agents-manage-ui` to be installed for visual agents orchestration.

  ```bash
  inkeep dev
  ```

  **Options:**

  * `--port <port>` - Port to run the server on (default: 3000)
  * `--host <host>` - Host to bind the server to (default: localhost)
  * `--build` - Build the Dashboard UI for production (packages standalone build)
  * `--export` - Export the Next.js project source files
  * `--output-dir <dir>` - Output directory for build files (default: ./inkeep-dev)
  * `--path` - Output the path to the Dashboard UI

  **Examples:**

  ```bash
  # Start development server
  inkeep dev

  # Start on custom port and host
  inkeep dev --port 3001 --host 0.0.0.0

  # Build for production (packages standalone build)
  inkeep dev --build --output-dir ./build

  # Export Next.js project source files
  inkeep dev --export --output-dir ./my-dashboard

  # Get dashboard path for deployment
  DASHBOARD_PATH=$(inkeep dev --path)
  echo "Dashboard built at: $DASHBOARD_PATH"

  # Use with Vercel
  vercel --cwd $(inkeep dev --path) -Q .vercel build

  # Use with Docker
  docker build -t inkeep-dashboard $(inkeep dev --path)

  # Use with other deployment tools
  rsync -av $(inkeep dev --path)/ user@server:/var/www/dashboard/
  ```
</SkillRule>

<SkillRule id="cli-config-command" skills="typescript-sdk" title="CLI inkeep config Command" description="Manage Inkeep configuration values">
  ### `inkeep config`

  Manage Inkeep configuration values.

  **Subcommands:**

  #### `inkeep config get [key]`

  Get configuration value(s).

  ```bash
  inkeep config get [key]
  ```

  **Options:**

  * `--config <path>` - Path to configuration file
  * `--config-file-path <path>` - Path to configuration file (deprecated, use --config)

  **Examples:**

  ```bash
  # Get all config values
  inkeep config get

  # Get specific value
  inkeep config get tenantId
  ```

  #### `inkeep config set <key> <value>`

  Set a configuration value.

  ```bash
  inkeep config set <key> <value>
  ```

  **Options:**

  * `--config <path>` - Path to configuration file
  * `--config-file-path <path>` - Path to configuration file (deprecated, use --config)

  **Examples:**

  ```bash
  inkeep config set tenantId my-tenant-id
  inkeep config set apiUrl http://localhost:3002
  ```

  #### `inkeep config list`

  List all configuration values.

  ```bash
  inkeep config list
  ```

  **Options:**

  * `--config <path>` - Path to configuration file
  * `--config-file-path <path>` - Path to configuration file (deprecated, use --config)
</SkillRule>

<SkillRule id="cli-profile-command" skills="typescript-sdk" title="CLI inkeep profile Command" description="Manage named CLI profiles for multiple remotes, credentials, and environments">
  ### `inkeep profile`

  Manage named CLI profiles for multiple remotes, credentials, and environments. Profiles are stored in `~/.inkeep/profiles.yaml`. A default `cloud` profile points to hosted endpoints.

  ```yaml
  # ~/.inkeep/profiles.yaml
  activeProfile: cloud
  profiles:
    cloud:
      remote: cloud
      credential: inkeep-cloud
      environment: production
    local:
      remote:
        api: http://localhost:3002
        manageUi: http://localhost:3000
      credential: none
      environment: development
  ```

  **Subcommands:**

  * `inkeep profile list` — Show all profiles and the active one.
  * `inkeep profile add <name>` — Interactive profile creation. Choose between Inkeep Cloud, Local (localhost defaults, no auth required), or Custom (self-hosted/staging URLs).
  * `inkeep profile use <name>` — Switch the active profile.
  * `inkeep profile current` — Show the active profile details and credential key.
  * `inkeep profile remove <name>` — Delete a profile (cannot remove the active one).

  **Examples:**

  ```bash
  # Create a local profile and switch to it
  inkeep profile add local
  inkeep profile use local

  # Inspect which profile is active
  inkeep profile current

  # List all profiles
  inkeep profile list

  # Remove an unused profile
  inkeep profile remove staging
  ```

  **How profiles are used:**

  * `inkeep login --profile <name>` stores credentials under the profile’s credential key.
  * Commands that talk to the APIs (`push`, `pull`, `status`, `login`, `logout`) honor `--profile <name>`; if omitted, the active profile is used.
  * Each profile bundles remote URLs, an environment name, and a credential reference so you can move between remote instances without editing config files.

  #### Using profiles with authenticated deployments

  If your deployment requires authentication (remote instance or self-hosted), run `inkeep login` for each profile so its credential slot in `profiles.yaml` has a token in the system keychain:

  ```bash
  # Authenticate against your secured deployment
  inkeep login --profile local

  # Validate which profile is active before running commands
  inkeep profile current

  # Use the profile when pushing/pulling
  inkeep push --profile local
  inkeep pull --profile local
  ```

  * The `credential` field in the profile is the key used to store the token; `login` writes to that key and `push/pull/status` read from it.
  * Repeat `login --profile <name>` for every profile/remote you operate against.
  * Use `inkeep logout --profile <name>` to clear a profile’s stored credentials if you need to rotate access.
</SkillRule>

<SkillRule id="cli-login-command" skills="typescript-sdk" title="CLI inkeep login Command" description="Authenticate with a deployment using the device code flow">
  ### `inkeep login`

  Authenticate with a deployment using the device code flow. Opens a browser window where you approve access, then stores credentials in your system keychain.

  ```bash
  inkeep login
  ```

  **Options:**

  * `--profile <name>` — Authenticate against a specific profile (defaults to active profile)

  Credentials are stored under the profile's `credential` key. Local profiles with `credential: none` do not require login.
</SkillRule>

<SkillRule id="cli-logout-command" skills="typescript-sdk" title="CLI inkeep logout Command" description="Clear stored credentials for a profile">
  ### `inkeep logout`

  Clear stored credentials for a profile.

  ```bash
  inkeep logout
  ```

  **Options:**

  * `--profile <name>` — Clear credentials for a specific profile (defaults to active profile)
</SkillRule>

<SkillRule id="cli-add-command" skills="typescript-sdk" title="CLI inkeep add Command" description="Pull a template project or MCP from the Inkeep Agents Cookbook">
  ### `inkeep add`

  Pull a template project or MCP from the [Inkeep Agents Cookbook](https://github.com/inkeep/agents/tree/main/agents-cookbook).

  ```bash
  inkeep add [options]
  ```

  **Options:**

  * `--project <name>` - Add a project template
  * `--mcp <name>` - Add a custom MCP server template for common use cases (adds server code to your project)
  * `--ui [component-id]` - Add a data or artifact UI component to your app (writes to `apps/agents-ui/src/ui` by default). Use the component id from the dashboard. Omit the id to see available components.
  * `--ui --list` - List available data and artifact components that have render code (same ids as in the dashboard “Use in your app” flow).
  * `--target-path <path>` - Target path to add the template to
  * `--config <path>` - Path to configuration file
  * `--profile <name>` - Profile to use for auth (with `--ui`)
  * `--quiet` - Suppress non-essential output (with `--ui`)

  **Examples:**

  ```bash
  # List available templates (both project and MCP)
  inkeep add

  # Add a project template
  inkeep add --project activities-planner

  # Add project template to specific path
  inkeep add --project activities-planner --target-path ./examples

  # Add a custom MCP server template (auto-detects apps/mcp/app directory)
  inkeep add --mcp zendesk

  # Add MCP server template to specific path
  inkeep add --mcp zendesk --target-path ./apps/mcp/app

  # Using specific config file
  inkeep add --project activities-planner --config ./my-config.ts

  # List addable UI components (data/artifact components with render code)
  inkeep add --ui --list

  # Add a single UI component by id (id from dashboard)
  inkeep add --ui weather-forecast
  ```

  **Behavior:**

  * When adding an MCP server template without `--target-path`, the CLI automatically searches for an `apps/mcp/app` directory in your project
  * If no app directory is found, you'll be prompted to confirm whether to add to the current directory
  * Project templates are added to the current directory or specified `--target-path`
  * Model configurations are automatically injected based on available API keys in your environment (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GOOGLE_GENERATIVE_AI_API_KEY`)
  * For `--ui`: requires authentication (same as push/pull). The component file is written with an export ensured on the main component. Register the component in your chat widget with the dashboard component **name** as the key and your imported component as the value (e.g. `components: { "Weather Forecast": WeatherForecast }`).
</SkillRule>

<SkillRule id="cli-update-command" skills="typescript-sdk" title="CLI inkeep update Command" description="Update the Inkeep CLI to the latest version">
  ### `inkeep update`

  Update the Inkeep CLI to the latest version from npm.

  ```bash
  inkeep update
  ```

  **Options:**

  * `--check` - Check for updates without installing
  * `--force` - Force update even if already on latest version

  **How it Works:**

  The update command automatically:

  1. **Detects Package Manager**: Identifies which package manager (npm, pnpm, bun, or yarn) was used to install the CLI globally
  2. **Checks Version**: Compares your current version with the latest available on npm
  3. **Updates CLI**: Executes the appropriate update command for your package manager
  4. **Displays Changelog**: Shows a link to the changelog for release notes

  **Examples:**

  ```bash
  # Check if an update is available (no installation)
  inkeep update --check

  # Update to latest version (with confirmation prompt)
  inkeep update

  # Force reinstall current version
  inkeep update --force

  # Skip confirmation prompt (useful for CI/CD)
  inkeep update --force
  ```

  **Output Example:**

  ```
  📦 Version Information:
    • Current version: 0.22.3
    • Latest version:  0.23.0

  📖 Changelog:
    • https://github.com/inkeep/agents/blob/main/agents-cli/CHANGELOG.md

  🔍 Detected package manager: pnpm

  ✅ Updated to version 0.23.0
  ```

  **Troubleshooting:**

  If you encounter permission errors, try running with elevated permissions:

  ```bash
  # For npm, pnpm, yarn
  sudo inkeep update

  # For bun
  sudo -E bun add -g @inkeep/agents-cli@latest
  ```

  **Package Manager Detection:**

  The CLI automatically detects which package manager you used by checking global package installations:

  * npm: Checks `npm list -g @inkeep/agents-cli`
  * pnpm: Checks `pnpm list -g @inkeep/agents-cli`
  * bun: Checks `bun pm ls -g`
  * yarn: Checks `yarn global list`

  If automatic detection fails, the CLI will prompt you to select your package manager.
</SkillRule>

<SkillRule id="cli-config-file-structure" skills="typescript-sdk" title="CLI Configuration File Structure" description="Structure of inkeep.config.ts and configuration priority">
  ## Configuration File

  The CLI uses a configuration file (typically `inkeep.config.ts`) to store settings:

  ```typescript
  import { defineConfig } from "@inkeep/agents-cli/config";

  export default defineConfig({
    tenantId: "your-tenant-id",
    agentsApi: {
      url: "http://localhost:3002",
    },
  });
  ```

  ### Configuration Priority

  The CLI resolves configuration values in the following order. Higher-priority sources override lower ones:

  **Local development (with a profile):**

  | Priority    | Source             | Example                                                                                |
  | ----------- | ------------------ | -------------------------------------------------------------------------------------- |
  | 1 (highest) | CLI flags          | `--tenant-id`, `--agents-api-url`                                                      |
  | 2           | Active profile     | API URLs, API key, and tenant ID from `~/.inkeep/profiles.yaml` + keychain credentials |
  | 3           | `inkeep.config.ts` | `tenantId`, `agentsApi.url`                                                            |
  | 4 (lowest)  | Built-in defaults  | Default API URLs                                                                       |

  When a profile is active, it overrides `inkeep.config.ts` for these fields:

  * `agentsApiUrl` — from `profile.remote.api`
  * `manageUiUrl` — from `profile.remote.manageUi`
  * `agentsApiKey` — from the profile's keychain credential
  * `tenantId` — from the profile's authenticated organization ID

  Other `inkeep.config.ts` values (like `outputDirectory`) are still respected.

  **CI/CD (no profile):**

  In CI environments, profiles are skipped entirely. The precedence becomes:

  | Priority    | Source                | Example                                                       |
  | ----------- | --------------------- | ------------------------------------------------------------- |
  | 1 (highest) | CLI flags             | `--tenant-id`, `--agents-api-url`                             |
  | 2           | Environment variables | `INKEEP_API_KEY`, `INKEEP_TENANT_ID`, `INKEEP_AGENTS_API_URL` |
  | 3           | `inkeep.config.ts`    | `tenantId`, `agentsApi.url`                                   |
  | 4 (lowest)  | Built-in defaults     | Default API URLs                                              |

  <Tip>
    Use `--profile <name>` on any command to override the active profile for a single invocation. This is useful for pushing to staging while your default profile points to production.
  </Tip>
</SkillRule>

<SkillRule id="cli-environment-variables" skills="typescript-sdk" title="CLI Environment Variables" description="Environment variables respected by the CLI and SDK">
  ## Environment Variables

  The CLI and SDK respect the following environment variables:

  * `INKEEP_TENANT_ID` - Tenant identifier
  * `INKEEP_AGENTS_API_URL` - Agents API base URL
  * `INKEEP_ENV` - Environment name for credentials loading during `inkeep push`
  * `INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET` - Optional bearer for Manage API (advanced)
  * `INKEEP_AGENTS_RUN_API_BYPASS_SECRET` - Optional bearer for Run API (advanced)
</SkillRule>

## Troubleshooting

**Project Not Found:**

* Projects are automatically managed based on your tenantId
* `inkeep push` will create resources as needed

### Getting Help

For additional help with any command:

```bash
inkeep [command] --help
```

For issues or feature requests, visit: [GitHub Issues](https://github.com/inkeep/agents/issues)


# Configure Runtime Limits (/typescript-sdk/configure-runtime-limits)

Customize execution limits, timeouts, and constraints for your deployment



## Overview

The Inkeep Agent Framework includes sensible defaults for execution limits, timeouts, and constraints. However, certain use cases—like long-running orchestration agents—may require different configurations.

<SkillRule id="runtime-limits-overview" skills="typescript-sdk" title="Runtime Limits Configuration" description="How to configure execution limits, timeouts, and constraints">
  ## How Runtime Configuration Works

  ### Two Types of Limits

  **1. API-Configurable Limits** (set per agent/sub-agent via manage-api)

  * Configured in your agent definitions using `stopWhen`
  * Controls agent-specific behavior
  * See [Agent Settings - StopWhen](/typescript-sdk/agent-settings#configuring-stopwhen)

  **2. Runtime Environment Limits** (set via environment variables)

  * System-wide defaults and maximum allowed values
  * Controls infrastructure-level constraints
  * Documented in this guide

  ### Configuration Hierarchy

  When an agent executes:

  1. Uses agent-specific `stopWhen` settings if configured
  2. Falls back to runtime environment defaults
  3. Cannot exceed runtime environment maximums

  ### Understanding the Constants Architecture

  The runtime environment limits are defined as TypeScript constants in the `@inkeep/agents-core` package. These constants are organized into two categories based on their purpose:

  **1. Schema Validation Constants** (`packages/agents-core/src/constants/schema-validation/defaults.ts`)

  * Define **API-level validation limits** (min, max, default values)
  * Control what values users can configure via the manage-api
  * Used in Zod schemas to validate agent configuration requests
  * Examples: Transfer count limits, generation step limits, prompt character limits

  **2. Runtime Execution Constants - Shared** (`packages/agents-core/src/constants/execution-limits-shared/defaults.ts`)

  * Define **internal runtime behavior** limits
  * Control infrastructure-level timeouts, retries, and constraints
  * Shared across both manage-api and run-api services
  * Examples: MCP connection timeouts, conversation history token limits

  **3. Runtime Execution Constants - Run-API** (`packages/agents-run-api/src/constants/execution-limits/defaults.ts`)

  * Define **run-api specific runtime behavior** limits
  * Control execution engine timeouts, retries, and resource constraints
  * Only used by the run-api service (not shared with manage-api)
  * Examples: LLM generation timeouts, function tool sandbox limits, streaming buffer sizes

  Each constant includes detailed inline documentation explaining its purpose, behavior, and impact on the runtime. **The TypeScript constant files are the source of truth** for default values and available overrides—not `.env.example`.

  All constants can be overridden via environment variables prefixed with `AGENTS_`. For example:

  ```bash
  # Override a schema validation constant
  AGENTS_AGENT_EXECUTION_TRANSFER_COUNT_DEFAULT=50

  # Override a runtime execution constant
  AGENTS_MCP_TOOL_CONNECTION_TIMEOUT_MS=5000
  ```

  ## Use Case: Long-Running Orchestration Agents

  For agents that orchestrate complex workflows (like coding agents that run for hours), you'll want to increase various limits and timeouts.

  ### Recommended Configuration

  Add these to your `.env` file:

  ```bash
  # Agent Execution - Allow more sub-agent transfers for complex orchestration
  AGENTS_EXECUTION_TRANSFER_COUNT_DEFAULT=50  # Default: 10
  AGENTS_EXECUTION_TRANSFER_COUNT_MAX=2000    # Default: 1000

  # Sub-Agent Turns - Allow more LLM generation steps per turn
  AGENTS_SUB_AGENT_TURN_GENERATION_STEPS_DEFAULT=30  # Default: 12
  AGENTS_SUB_AGENT_TURN_GENERATION_STEPS_MAX=2000    # Default: 1000

  # LLM Timeouts - Increase for longer-running operations
  AGENTS_LLM_GENERATION_FIRST_CALL_TIMEOUT_MS_STREAMING=600000   # 10min, Default: 4.5min
  AGENTS_LLM_GENERATION_SUBSEQUENT_CALL_TIMEOUT_MS=180000        # 3min, Default: 1.5min

  # Function Tools - Allow longer execution for complex operations
  AGENTS_FUNCTION_TOOL_EXECUTION_TIMEOUT_MS_DEFAULT=120000  # 2min, Default: 30sec

  # MCP Tools - Increase timeout for external tool operations
  AGENTS_MCP_TOOL_REQUEST_TIMEOUT_MS_DEFAULT=180000  # 3min, Default: 1min

  # Session Management - Keep tool results cached longer
  AGENTS_SESSION_TOOL_RESULT_CACHE_TIMEOUT_MS=1800000  # 30min, Default: 5min

  # Streaming - Allow streams to run longer
  AGENTS_STREAM_MAX_LIFETIME_MS=1800000  # 30min, Default: 10min
  ```

  <Note>
    `AGENTS_EXECUTION_TRANSFER_COUNT_DEFAULT` sets the default number of transfers allowed between sub-agents in a turn when no agent-specific limit is configured. `AGENTS_EXECUTION_TRANSFER_COUNT_MAX` sets the maximum number of transfers that can be configured via the manage-api for any agent.

    The same pattern applies to `AGENTS_SUB_AGENT_TURN_GENERATION_STEPS_DEFAULT` and `AGENTS_SUB_AGENT_TURN_GENERATION_STEPS_MAX`.
  </Note>

  ### Why These Settings?

  * **Transfer Count**: Coding orchestration often involves many handoffs between specialist agents (planning → implementation → testing → refinement)
  * **Generation Steps**: Each coding operation may require multiple tool calls (read files, analyze, write code, run tests)
  * **Timeouts**: Code generation and analysis can take time, especially for large files or complex refactoring
  * **Cache Duration**: Keeps tool results available throughout long sessions for artifact processing
</SkillRule>

<SkillRule id="runtime-limits-variables" skills="typescript-sdk" title="Runtime Configuration Variables" description="All available environment variables for configuring runtime limits">
  ## All Available Configuration Variables

  All runtime configuration variables are defined as TypeScript constants. To explore available overrides and their detailed documentation:

  1. **View the constant files** (source of truth):
     * Schema validation constants: `packages/agents-core/src/constants/schema-validation/defaults.ts`
     * Runtime execution constants (shared): `packages/agents-core/src/constants/execution-limits-shared/defaults.ts`
     * Runtime execution constants (run-api): `packages/agents-run-api/src/constants/execution-limits/defaults.ts`
  2. Each constant includes inline documentation explaining its purpose, behavior, and impact
  3. Convert constant names to environment variables by prefixing with `AGENTS_`

  ### Configuration Categories

  The runtime configuration is organized into functional categories:

  **Schema Validation Constants** (API-level limits):

  * **Agent Execution Transfer Count**: Limits transfers between sub-agents in a turn
  * **Sub-Agent Turn Generation Steps**: LLM generation steps within a single sub-agent activation
  * **Status Update Thresholds**: Event and time-based triggers for status updates
  * **Prompt Validation**: Character limits for agent and sub-agent system prompts
  * **Context Fetchers**: HTTP timeouts for external data fetching (CRM lookups, API calls)

  **Runtime Execution Constants - Shared** (infrastructure-level limits):

  * **MCP Tool Connection & Retry**: Connection timeouts and exponential backoff for MCP tools
  * **Conversation History**: Token limits for conversation context windows

  **Runtime Execution Constants - Run-API** (run-api service-specific limits):

  * **LLM Generation Timeouts**: Timeout behavior for AI model calls
  * **Function Tool Sandbox**: Execution limits for function tools in isolated sandboxes
  * **Delegation & A2A**: Inter-agent communication settings
  * **Artifact Processing**: UI component generation with retry mechanisms
  * **Session Management**: Tool result caching and cleanup
  * **Streaming**: Frequency, batching, and lifetime limits for streams
</SkillRule>


# Context Fetchers in TypeScript SDK (/typescript-sdk/context-fetchers)

Define context fetchers in the TypeScript SDK to pull real-time data from external APIs into agent prompts.



## Overview

Context fetchers allow you to embed real-time data from external APIs into your agent prompts. Instead of hardcoding information in your agent prompt, context fetchers dynamically retrieve fresh data for each conversation.

## Key Features

* **Dynamic data retrieval**: Fetch real-time data from APIs.
* **Dynamic Prompting**: Use dynamic data in your agent prompts
* **Headers integration**: Use request-specific parameters to customize data fetching.
* **Data transformation**: Transform API responses into the exact format your agent needs.

<SkillRule id="context-fetchers-overview" skills="typescript-sdk" title="Context Fetchers Overview" description="How to use context fetchers to embed real-time data in agent prompts">
  ## Context Fetchers vs Tools

  * **Context Fetchers**: Pre-populate agent prompts with dynamic data
    * Run automatically before/during conversation startup
    * Data becomes part of the agent's system prompt
    * Perfect for: Personalized agent personas, dynamic agent guardrails
    * Example Prompt: `You are an assistant for ${userContext.toTemplate('user.name')} and you work for ${userContext.toTemplate('user.organization')}`

  * **Tools**: Enable agents to take actions or fetch data during conversations
    * Called by the agent when needed during the conversation
    * Agent decides when and how to use them
    * Example Tool Usage: Agent calls a "send\_email" tool or "search\_database" tool

  ## Basic Usage

  Let's create a simple context fetcher that retrieves user information:

  ```typescript
  import { agent, subAgent } from "@inkeep/agents-sdk";
  import { contextConfig, fetchDefinition, headers } from "@inkeep/agents-core";
  import { z } from "zod";

  // 1. Define a schema for headers validation. All header keys are converted to lowercase.
  // In this example all incoming headers will be validated to make sure they include user_id and api_key.
  const personalAgentHeaders = headers({
    schema: z.object({
      user_id: z.string(),
      api_key: z.string(),
    })
  });

  // 2. Create the fetcher with
  const userFetcher = fetchDefinition({
    id: "user-info",
    name: "User Information",
    trigger: "initialization", // Fetch only once when a conversation is started with the Agent. When set to "invocation", the fetch will be executed every time a request is made to the Agent.
    fetchConfig: {
      url: `https://api.example.com/users/${personalAgentHeaders.toTemplate('user_id')}`,
      method: "GET",
      headers: {
        Authorization: `Bearer ${personalAgentHeaders.toTemplate('api_key')}`,
      },
      transform: "user", // Extract user from response, for example if the response is { "user": { "name": "John Doe", "email": "john.doe@example.com" } }, the transform will return the user object
    },
    responseSchema: z.object({
      user: z.object({
        name: z.string(),
        email: z.string(),
      }),
    }), // Used to validate the result of the transformed api response.
    defaultValue: "Unable to fetch user information",
  });

  // 3. Configure context
  const personalAgentContext = contextConfig({
    headers: personalAgentHeaders,
    contextVariables: {
      user: userFetcher,
    },
  });

  // 4. Create and use the Sub Agent
  const personalAssistant = subAgent({
    id: "personal-assistant",
    name: "Personal Assistant",
    description: "A personalized AI assistant",
    prompt: `Hello ${personalAgentContext.toTemplate('user.name')}! I'm your personal assistant.`,
  });

  // Initialize the Agent
  export const myAgent = agent({
    id: "personal-agent",
    name: "Personal Assistant Agent",
    defaultSubAgent: personalAssistant,
    subAgents: () => [personalAssistant],
    contextConfig: personalAgentContext,
  });
  ```

  ## Using Context Variables

  Context variables can be used in your agent prompts using JSONPath template syntax `{{contextVariableKey.field_name}}`.
  Use the context config's `toTemplate()` method for type-safe templating with autocomplete and validation.

  ```typescript
  const personalGraphContext = contextConfig({
    headers: personalGraphHeaders,
    contextVariables: {
      user: userFetcher,
    },
  });

  const personalAgent = subAgent({
    id: "personal-agent",
    name: "Personal Assistant",
    description: "A personalized AI assistant",
    prompt: `Hello ${personalGraphContext.toTemplate('user.name')}! I'm your personal assistant.`,
  });
  ```

  Context variables are resolved using [JSONPath notation](https://jsonpath.com).

  ## Using Credentials

  If your fetch requires authentication via a stored credential, pass the `credentialReferenceId` to the `fetchDefinition`:

  ```typescript
  const projectInfoFetcher = fetchDefinition({
    id: "project-info",
    name: "Project Information",
    trigger: "initialization",
    credentialReferenceId: "u6zq98tz0lkloakuqbpdv",
    fetchConfig: {
      url: "https://api.example.com/graphql",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: {
        query: "query GetProjectInfo { project { name description } }",
      },
      transform: "data.project",
    },
    defaultValue: "Unable to fetch project information",
  });
  ```

  ### Finding the Credential Reference ID

  To find the credential reference ID:

  1. Navigate to the **Credentials** page in the Inkeep dashboard.
  2. Click on the credential you want to use.
  3. Copy the ID from the browser URL — it is the last path segment. For example, in `https://app.inkeep.com/.../credentials/u6zq98tz0lkloakuqbpdv`, the credential reference ID is `u6zq98tz0lkloakuqbpdv`.

  ## Data transformation

  The `transform` property on fetch definitions lets you extract exactly what you need from API responses using JSONPath notation:

  ```typescript
  // API returns: { "user": { "profile": { "displayName": "John Doe" } } }
  transform: "user.profile.displayName"; // Result: "John Doe"

  // API returns: { "items": [{ "name": "First Item" }, { "name": "Second Item" }] }
  transform: "items[0].name"; // Result: "First Item"
  ```

  ## Optional Context Fetches

  Sometimes you want to fetch context data only when certain conditions are met—for example, when a specific header is provided. The `requiredToFetch` property lets you specify template variables that must resolve to non-empty values for the fetch to execute.

  If any required variable is missing or resolves to an empty string, the fetch is **skipped** (not treated as an error), and the `defaultValue` is used if provided.

  ```typescript
  import { agent, subAgent } from "@inkeep/agents-sdk";
  import { contextConfig, fetchDefinition, headers } from "@inkeep/agents-core";
  import { z } from "zod";

  // Define headers schema - x-conversation-id is optional
  const chatHeaders = headers({
    schema: z.object({
      "x-tenant-id": z.string(),
      "x-project-id": z.string(),
      "x-conversation-id": z.string().optional(), // Optional header
    })
  });

  // This fetch only runs when x-conversation-id header is provided
  const conversationHistoryFetcher = fetchDefinition({
    id: "conversation-history",
    name: "Conversation History",
    trigger: "initialization",
    fetchConfig: {
      url: `https://api.example.com/conversations/${chatHeaders.toTemplate('x-conversation-id')}`,
      method: "GET",
      // Only fetch if x-conversation-id header is present and non-empty
      requiredToFetch: [chatHeaders.toTemplate('x-conversation-id')],
    },
    responseSchema: z.object({
      messages: z.array(z.object({
        role: z.string(),
        content: z.string(),
      })),
    }),
    defaultValue: { messages: [] }, // Used when header is not provided
  });

  const chatContext = contextConfig({
    headers: chatHeaders,
    contextVariables: {
      history: conversationHistoryFetcher,
    },
  });
  ```

  **When to use `requiredToFetch`:**

  * Fetching user-specific data when an optional user ID header is provided
  * Loading conversation history only when continuing an existing conversation
  * Conditional data loading based on optional API keys or authentication headers
</SkillRule>

## Best Practices

1. **Use Appropriate Triggers**

   * `initialization`: Use when data rarely changes
   * `invocation`: Use for frequently changing data

2. **Handle Errors Gracefully**

   * Always provide a `defaultValue`
   * Use appropriate response schemas

3. **Use `requiredToFetch` for Optional Dependencies**

   * Specify required template variables for conditional fetches
   * Provide a `defaultValue` to handle skipped fetches gracefully

## Related documentation

* [Headers](/typescript-sdk/headers) - Learn how to pass dynamic context to your agents via HTTP headers


# Data Operations (/typescript-sdk/data-operations)

Learn about data operations emitted by agents and how to use the x-emit-operations header to control their visibility.



<SkillRule id="data-operations-overview" skills="typescript-sdk" title="Data Operations Overview" description="How to enable and use data operations for debugging and monitoring">
  ## Overview

  Data operations are detailed, real-time events that provide visibility into what agents are doing during execution. They include agent reasoning, tool executions, transfers, delegations, and artifact creation. By default, these operations are hidden from end users to keep the interface clean, but they can be enabled for debugging and monitoring purposes.

  ## The x-emit-operations Header

  The `x-emit-operations` header controls whether data operations are included in the response stream. When set to `true`, the system will emit detailed operational events alongside the regular response content.

  ### Usage

  ```bash
  curl -N \
    -X POST "http://localhost:3003/api/chat" \
    -H "Authorization: Bearer $INKEEP_API_KEY" \
    -H "Content-Type: application/json" \
    -H "x-emit-operations: true" \
    -d '{
      "messages": [
        { "role": "user", "content": "What can you do?" }
      ],
      "conversationId": "chat-1234"
    }'
  ```

  ### CLI Usage

  In the CLI, you can toggle data operations using the `operations` command:

  ```bash
  # Start a chat session
  inkeep chat

  # Toggle data operations on/off
  > operations
  🔧 Emit operations: ON
  Data operations will be shown during responses.

  > operations
  🔧 Emit operations: OFF
  Data operations are hidden.
  ```
</SkillRule>

<SkillRule id="data-operation-types" skills="typescript-sdk" title="Data Operation Types Reference" description="All data operation event types and their payloads">
  ## Data Operation Types

  ### Agent Events

  #### `agent_generate`

  Emitted when an agent generates content — text, structured data, or a mix of both.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "agent_generate",
      "label": "Agent search-agent generating response",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "search-agent",
        "data": {
          "parts": [
            {
              "type": "text",
              "content": "I found 5 relevant documents..."
            }
          ],
          "generationType": "text_generation"
        }
      }
    }
  }
  ```

  **`generationType` values:**

  | Value               | Meaning                                                                                                                                                                                                                                                            |
  | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | `text_generation`   | The response is text only — no structured data components were emitted.                                                                                                                                                                                            |
  | `object_generation` | The response is a structured object only — the agent emitted a data component that matches a `dataComponents` schema, with no accompanying prose.                                                                                                                  |
  | `mixed_generation`  | The response contains **both** text and one or more structured data components. Emitted when an agent configured with `dataComponents` also produces prose (for example, a reasoning prelude before a structured result, or narration alongside a data component). |

  **`data.parts[].type` values:**

  | Value            | Payload fields       | Notes                                                                                  |
  | ---------------- | -------------------- | -------------------------------------------------------------------------------------- |
  | `text`           | `content`            | Prose emitted by the agent.                                                            |
  | `data_component` | `data`               | A structured data component matching one of the agent's `dataComponents` schemas.      |
  | `data_artifact`  | `data`               | A structured artifact (has both `artifactId` and `toolCallId` on `data`).              |
  | `tool_call`      | `toolName`, `args`   | Internal tool invocation (rare in `agent_generate`; more common in `agent_reasoning`). |
  | `tool_result`    | `toolName`, `result` | Internal tool result (rare in `agent_generate`; more common in `agent_reasoning`).     |

  When `generationType` is `mixed_generation`, `data.parts` is ordered by emission: a text part may precede a data component, appear between components, or follow them. Consumers that render agent responses should iterate `parts` in order and render each entry according to its `type`.

  #### `agent_reasoning`

  Emitted when an agent is reasoning through a request or planning its approach.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "agent_reasoning",
      "label": "Agent search-agent reasoning through request",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "search-agent",
        "data": {
          "parts": [
            {
              "type": "text",
              "content": "I need to search for information about..."
            }
          ]
        }
      }
    }
  }
  ```

  ### Tool Execution Events

  #### `tool_call`

  Emitted when an agent starts calling a tool or function.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "tool_call",
      "label": "Tool call: search_documents",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "search-agent",
        "data": {
          "toolName": "search_documents",
          "args": {
            "query": "machine learning best practices",
            "limit": 10
          },
          "toolCallId": "call_abc123",
          "toolId": "tool_xyz789"
        }
      }
    }
  }
  ```

  **Tool Approval Events**

  When a tool requires user approval, the `tool_call` event includes additional fields:

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "tool_call",
      "label": "Tool call: delete_user_data",
      "details": {
        "timestamp": 1726247200000,
        "subAgentId": "data-agent",
        "data": {
          "toolName": "delete_user_data",
          "input": {
            "userId": "user_123",
            "confirm": true
          },
          "toolCallId": "call_approval_xyz789",
          "needsApproval": true,
          "conversationId": "conv_abc123"
        }
      }
    }
  }
  ```

  **Approval-specific fields:**

  * `needsApproval: true` - Indicates this tool requires user approval before execution
  * `conversationId` - The conversation context for sending approval responses

  See [Tool approvals](/typescript-sdk/tools/tool-approvals) for complete configuration and integration details.

  #### `tool_result`

  Emitted when a tool execution completes (success or failure). Tool results can include text, structured data, or multimedia content such as images.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "tool_result",
      "label": "Tool result: search_documents",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "search-agent",
        "data": {
          "toolName": "search_documents",
          "result": {
            "documents": [
              {
                "title": "ML Best Practices Guide",
                "url": "/docs/ml-guide",
                "relevance": 0.95
              }
            ]
          },
          "toolCallId": "call_abc123",
          "toolId": "tool_xyz789",
          "duration": 1250
        }
      }
    }
  }
  ```

  **Error Example:**

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "tool_result",
      "label": "Tool error: search_documents",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "search-agent",
        "data": {
          "toolName": "search_documents",
          "result": null,
          "toolCallId": "call_abc123",
          "toolId": "tool_xyz789",
          "duration": 500,
          "error": "API rate limit exceeded"
        }
      }
    }
  }
  ```

  **Image Result Example:**

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "tool_result",
      "label": "Tool result: generate_chart",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "analysis-agent",
        "data": {
          "toolName": "generate_chart",
          "result": {
            "image": "blob://v1/t_org123/media/p_proj456/conv/c_conv789/m_msg012/sha256-a1b2c3d4.png",
            "title": "Q4 Sales Performance"
          },
          "toolCallId": "call_xyz789",
          "toolId": "tool_chart123",
          "duration": 2100
        }
      }
    }
  }
  ```

  <Note>
    The `blob://` URI is an internal storage reference. When retrieved via the API or conversation history, these are automatically resolved to accessible media proxy URLs. See [Images in Memory](/typescript-sdk/memory#images-in-memory).
  </Note>

  ### Agent Interaction Events

  #### `transfer`

  Emitted when control is transferred from one agent to another.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "transfer",
      "label": "Agent transfer: search-agent → analysis-agent",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "search-agent",
        "data": {
          "fromSubAgent": "search-agent",
          "targetAgent": "analysis-agent",
          "reason": "Specialized analysis required",
          "context": {
            "searchResults": "...",
            "userQuery": "..."
          }
        }
      }
    }
  }
  ```

  #### `delegation_sent`

  Emitted when an agent delegates a task to another agent.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "delegation_sent",
      "label": "Task delegated: coordinator-agent → search-agent",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "coordinator-agent",
        "data": {
          "delegationId": "deleg_xyz789",
          "fromSubAgent": "coordinator-agent",
          "targetAgent": "search-agent",
          "taskDescription": "Search for information about machine learning",
          "context": {
            "priority": "high",
            "deadline": "2024-01-15T10:00:00Z"
          }
        }
      }
    }
  }
  ```

  #### `delegation_returned`

  Emitted when a delegated task is completed and returned.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "delegation_returned",
      "label": "Task completed: search-agent → coordinator-agent",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "search-agent",
        "data": {
          "delegationId": "deleg_xyz789",
          "fromSubAgent": "search-agent",
          "targetAgent": "coordinator-agent",
          "result": {
            "status": "completed",
            "documents": [...],
            "summary": "Found 5 relevant documents"
          }
        }
      }
    }
  }
  ```

  ### Artifact Events

  #### `artifact_saved`

  Emitted when an agent creates or saves an artifact (document, chart, file, etc.).

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "artifact_saved",
      "label": "Artifact saved: chart",
      "details": {
        "timestamp": 1726247200000,
        "agentId": "analysis-agent",
        "data": {
          "artifactId": "art_123456",
          "taskId": "task_789",
          "toolCallId": "tool_abc123",
          "artifactType": "chart",
          "summaryData": {
            "title": "Sales Performance Q4 2023",
            "type": "bar_chart"
          },
          "fullData": {
            "chartData": [...],
            "config": {...}
          },
          "metadata": {
            "createdBy": "analysis-agent",
            "version": "1.0"
          }
        }
      }
    }
  }
  ```

  **Field notes:**

  * `summaryData` — contains the artifact's **preview fields** (those marked `inPreview: true` in the schema). These are available immediately in the agent's context and streamed to clients.
  * `fullData` — contains all remaining non-preview fields. This data is persisted in storage and retrievable on demand.

  See [Artifact Components](/typescript-sdk/structured-outputs/artifact-components) for how to define preview and non-preview fields in your artifact schemas.

  ## System Events

  ### `agent_initializing`

  Emitted when the agent runtime is starting up.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "agent_initializing",
      "details": {
        "sessionId": "session_abc123",
        "agentId": "graph_xyz789"
      }
    }
  }
  ```

  ### `completion`

  Emitted when an agent completes its task.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "completion",
      "details": {
        "agent": "search-agent",
        "iteration": 1
      }
    }
  }
  ```

  ### `error`

  Emitted when an error occurs during execution.

  ```json
  {
    "type": "data-operation",
    "data": {
      "type": "error",
      "message": "Tool execution failed: API rate limit exceeded",
      "agent": "search-agent",
      "severity": "error",
      "code": "RATE_LIMIT_EXCEEDED",
      "timestamp": 1726247200000
    }
  }
  ```

  ## Example: Complete Request with Data Operations

  Here's a complete example showing a request with data operations enabled:

  ```bash
  curl -N \
    -X POST "http://localhost:3003/api/chat" \
    -H "Authorization: Bearer $INKEEP_API_KEY" \
    -H "Content-Type: application/json" \
    -H "x-emit-operations: true" \
    -d '{
      "messages": [
        { "role": "user", "content": "Create a sales report for Q4" }
      ],
      "conversationId": "chat-1234"
    }'
  ```

  **Response Stream:**

  ```text
  data: {"type":"agent_initializing","details":{"sessionId":"session_abc123","agentId":"graph_xyz789"}}

  data: {"type":"data-operation","data":{"type":"agent_reasoning","label":"Agent coordinator-agent reasoning through request","details":{"timestamp":1726247200000,"agentId":"coordinator-agent","data":{"parts":[{"type":"text","content":"I need to create a sales report for Q4. This will require gathering data and generating a chart."}]}}}}

  data: {"type":"data-operation","data":{"type":"tool_call","label":"Tool call: get_sales_data","details":{"timestamp":1726247200000,"agentId":"coordinator-agent","data":{"toolName":"get_sales_data","args":{"quarter":"Q4","year":"2023"},"toolCallId":"call_abc123","toolId":"tool_xyz789"}}}}

  data: {"type":"data-operation","data":{"type":"tool_result","label":"Tool result: get_sales_data","details":{"timestamp":1726247200000,"agentId":"coordinator-agent","data":{"toolName":"get_sales_data","result":{"sales":[...]},"toolCallId":"call_abc123","toolId":"tool_xyz789","duration":850}}}}

  data: {"type":"data-artifact","data":{ ... }}

  data: {"type":"data-operation","data":{"type":"artifact_saved","label":"Artifact saved: chart","details":{"timestamp":1726247200000,"agentId":"coordinator-agent","data":{"artifactId":"art_123456","artifactType":"chart","summaryData":{"title":"Q4 Sales Report"}}}}}

  data: {"type":"text-start","id":"1726247200-abc123"}

  data: {"type":"text-delta","id":"1726247200-abc123","delta":"I've created a comprehensive Q4 sales report..."}

  data: {"type":"text-end","id":"1726247200-abc123"}

  data: {"type":"completion","details":{"agent":"coordinator-agent","iteration":1}}
  ```

  This provides complete visibility into the agent's execution process, from initialization through reasoning, tool execution, artifact creation, and final response generation.
</SkillRule>


# Evaluations in TypeScript SDK (/typescript-sdk/evaluations)

Manage evaluators, test suites, and batch evaluations programmatically with the TypeScript SDK



The TypeScript SDK provides an **EvaluationClient** that talks to the Evaluations API so you can manage evaluators, test suites, run configurations, trigger runs with scoring, and read results—all from code.

For full endpoint details and request/response shapes, see the [Evaluations API reference](/api-reference/evaluations).

## Setup: create a client

```typescript
import { EvaluationClient } from "@inkeep/agents-sdk";

const client = new EvaluationClient({
  tenantId: process.env.INKEEP_TENANT_ID!,
  projectId: process.env.INKEEP_PROJECT_ID!,
  apiUrl: "https://api.inkeep.com",
  apiKey: process.env.INKEEP_API_KEY,
});
```

| Parameter   | Type   | Required | Description                                                          |
| ----------- | ------ | -------- | -------------------------------------------------------------------- |
| `tenantId`  | string | Yes      | Your tenant (organization) ID                                        |
| `projectId` | string | Yes      | Your project ID                                                      |
| `apiUrl`    | string | Yes      | API base URL (e.g. `https://api.inkeep.com` or your self-hosted URL) |
| `apiKey`    | string | No       | Bearer token for authenticated requests                              |

## End-to-end example

The walkthrough below creates an evaluator, a test suite with items, a run configuration, triggers an evaluation, and checks the results. Each step builds on the previous one.

### Step 1 — Create an evaluator

An evaluator defines **how** to score agent output. You provide a prompt, a JSON schema for the structured result, and a model to run the evaluation.

The schema should include a numeric `score`, a boolean `passed`, and a `reasoning` string so you get both a quantitative metric and a human-readable explanation for every evaluation.

```typescript
const evaluator = (await client.createEvaluator({
  name: "Answer quality",
  description: "Checks correctness, helpfulness, and tone of agent replies",
  prompt: `You are an expert QA evaluator.

Given the full conversation between a user and an AI assistant, evaluate the **last assistant reply** on three dimensions:
1. **Correctness** — Is the information factually accurate? If an expected output is provided, does the reply match its intent?
2. **Helpfulness** — Does the reply fully address the user's question without unnecessary filler?
3. **Tone** — Is the reply professional, clear, and appropriately concise?

Return a JSON object with:
- "score": a number from 1 to 5 (1 = poor, 5 = excellent) reflecting overall quality.
- "passed": true if the reply is acceptable for production (score >= 4), false otherwise.
- "reasoning": 1-2 sentences explaining the score, citing specific strengths or issues.`,
  schema: {
    type: "object",
    properties: {
      score: {
        type: "number",
        description: "Overall quality score from 1 to 5",
      },
      passed: {
        type: "boolean",
        description: "Whether the reply meets production quality bar",
      },
      reasoning: {
        type: "string",
        description: "Brief explanation of the score",
      },
    },
    required: ["score", "passed", "reasoning"],
  },
  model: { model: "gpt-4o-mini" },
  passCriteria: {
    operator: "and",
    conditions: [{ field: "score", operator: ">=", value: 4 }],
  },
})) as { id: string };
```

| Field          | Type                 | Required | Description                                                                                                       |
| -------------- | -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `name`         | string               | Yes      | Display name                                                                                                      |
| `description`  | string               | No       | What this evaluator checks                                                                                        |
| `prompt`       | string               | Yes      | Instructions for the evaluation model                                                                             |
| `schema`       | object (JSON Schema) | Yes      | Structure of the evaluation output — typically includes `score`, `passed`, and `reasoning`                        |
| `model`        | object               | Yes      | `{ model: string, providerOptions?: object }`                                                                     |
| `passCriteria` | object               | No       | `{ operator: "and"\|"or", conditions: [{ field, operator, value }] }`. Operators: `>`, `<`, `>=`, `<=`, `=`, `!=` |

### Step 2 — Create a test suite and add items

A test suite is a named collection of items. Each item has **input** (the messages sent to the agent) and optional **expectedOutput** (reference answer).

```typescript
const testSuite = (await client.createDataset({
  name: "Support FAQs",
})) as { id: string };

await client.createDatasetItem(testSuite.id, {
  input: {
    messages: [
      { role: "user", content: { text: "How do I reset my password?" } },
    ],
  },
  expectedOutput: [
    { role: "assistant", content: { text: "Go to Settings → Security → Reset password." } },
  ],
});

await client.createDatasetItems(testSuite.id, [
  {
    input: {
      messages: [{ role: "user", content: { text: "What are your hours?" } }],
    },
  },
  {
    input: {
      messages: [{ role: "user", content: { text: "Do you offer refunds?" } }],
    },
  },
]);
```

### Step 3 — Create a run configuration

A **run configuration** ties the test suite to one or more **agents** and optional default **evaluators**. It is a saved "recipe" you can trigger repeatedly.

```typescript
const runConfig = (await client.createDatasetRunConfig({
  name: "Support smoke test",
  datasetId: testSuite.id,
  agentIds: ["your-agent-id"],
  evaluatorIds: [evaluator.id],
})) as { id: string };
```

| Field          | Type      | Required | Description                                                        |
| -------------- | --------- | -------- | ------------------------------------------------------------------ |
| `name`         | string    | Yes      | Display name for the config                                        |
| `description`  | string    | No       | Optional description                                               |
| `datasetId`    | string    | Yes      | The test suite to run                                              |
| `agentIds`     | string\[] | No       | Agents that process each item (at least one needed before trigger) |
| `evaluatorIds` | string\[] | No       | Default evaluators attached when you trigger a run                 |

### Step 4 — Trigger a run

Starting a run queues every item × agent combination. When evaluators are included, a **batch evaluation** job is automatically created for the resulting conversations.

```typescript
const started = await client.triggerDatasetRun(runConfig.id, {
  evaluatorIds: [evaluator.id],
});
// { datasetRunId: "...", status: "pending", totalItems: 3 }
```

The `evaluatorIds` on trigger is optional — omit it to use the defaults from the run configuration, or pass different ids to override.

### Step 5 — Check run status

```typescript
const run = await client.getDatasetRun(started.datasetRunId);
console.log(run);
```

Poll `getDatasetRun` or check the **Test Suites** page in the Visual Builder to watch items transition from pending to completed with evaluation scores.

### Step 6 (optional) — Evaluate a previous run after the fact

If you ran a test suite **without** evaluators and want to score those conversations later, trigger a batch evaluation scoped to the run:

```typescript
await client.triggerBatchEvaluation({
  evaluatorIds: [evaluator.id],
  datasetRunIds: [started.datasetRunId],
});
```

## Method reference

### Evaluators

| Method                                  | Purpose                                                             |
| --------------------------------------- | ------------------------------------------------------------------- |
| `createEvaluator(data)`                 | Create an evaluator (prompt, schema, model, optional pass criteria) |
| `listEvaluators()`                      | List all evaluators in the project                                  |
| `getEvaluator(evaluatorId)`             | Fetch one evaluator                                                 |
| `updateEvaluator(evaluatorId, partial)` | Update evaluator fields                                             |
| `deleteEvaluator(evaluatorId)`          | Delete an evaluator                                                 |

### Test suites and items

| Method                                            | Purpose                                                      |
| ------------------------------------------------- | ------------------------------------------------------------ |
| `listDatasets()`                                  | List test suites for the project                             |
| `getDataset(testSuiteId)`                         | Fetch one test suite                                         |
| `createDataset({ name })`                         | Create a test suite                                          |
| `updateDataset(testSuiteId, partial)`             | Update name, etc.                                            |
| `deleteDataset(testSuiteId)`                      | Delete a test suite and its items                            |
| `listDatasetItems(testSuiteId)`                   | List items                                                   |
| `getDatasetItem(testSuiteId, itemId)`             | Fetch one item                                               |
| `createDatasetItem(testSuiteId, itemData)`        | Create an item (`input` required; `expectedOutput` optional) |
| `createDatasetItems(testSuiteId, items[])`        | Bulk create                                                  |
| `updateDatasetItem(testSuiteId, itemId, partial)` | Update an item                                               |
| `deleteDatasetItem(testSuiteId, itemId)`          | Delete an item                                               |

### Run configurations and runs

| Method                                                                  | Purpose                                                                              |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `createDatasetRunConfig({ name, datasetId, agentIds?, evaluatorIds? })` | Create a run configuration (which agents run the suite; optional default evaluators) |
| `triggerDatasetRun(runConfigId, { evaluatorIds?, branchName? }?)`       | Start a run; returns `datasetRunId`, `status`, `totalItems`                          |
| `listDatasetRuns(testSuiteId)`                                          | List runs for a test suite                                                           |
| `getDatasetRun(runId)`                                                  | Fetch a run with items and conversations                                             |

### Batch evaluation

| Method                                                                                          | Purpose                                     |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `triggerBatchEvaluation({ evaluatorIds, name?, conversationIds?, dateRange?, datasetRunIds? })` | One-off batch evaluation over conversations |

| Option            | Type                                  | Required | Description                                       |
| ----------------- | ------------------------------------- | -------- | ------------------------------------------------- |
| `evaluatorIds`    | string\[]                             | Yes      | IDs of evaluators to run                          |
| `name`            | string                                | No       | Name for the job                                  |
| `conversationIds` | string\[]                             | No       | Limit to these conversations                      |
| `dateRange`       | `{ startDate, endDate }` (YYYY-MM-DD) | No       | Limit to conversations in this date range         |
| `datasetRunIds`   | string\[]                             | No       | Limit to conversations from these test suite runs |

### Evaluation suite configs (continuous tests)

Suite configs group evaluators and optional agent filters and sample rates for **continuous tests** that evaluate a fraction of live conversations automatically.

| Method                                                                 | Purpose                     |
| ---------------------------------------------------------------------- | --------------------------- |
| `createEvaluationSuiteConfig({ evaluatorIds, sampleRate?, filters? })` | Create a suite config       |
| `addEvaluatorToSuiteConfig(configId, evaluatorId)`                     | Add an evaluator            |
| `removeEvaluatorFromSuiteConfig(configId, evaluatorId)`                | Remove an evaluator         |
| `listEvaluationSuiteConfigEvaluators(configId)`                        | List evaluators on a config |

| Option         | Type      | Required | Description                                          |
| -------------- | --------- | -------- | ---------------------------------------------------- |
| `evaluatorIds` | string\[] | Yes      | At least one evaluator ID                            |
| `sampleRate`   | number    | No       | Fraction of matching conversations to evaluate (0–1) |
| `filters`      | object    | No       | Restrict scope, e.g. `{ agentIds: ["agent-id"] }`    |

To list results by job or run config, use the [Evaluations API](/api-reference/evaluations).

## Related

* [Evaluations API reference](/api-reference/evaluations) — Full list of evaluation endpoints and schemas
* [Visual Builder: Evaluations](/visual-builder/evaluations) — Configure evaluators, batch evaluations, and continuous tests in the UI
* [Test suites](/visual-builder/test-suites) — How test suites work in the Visual Builder


# Add External Agents to your Agent (/typescript-sdk/external-agents)

Learn how to configure and use external agents using the A2A protocol



<SkillRule id="external-agents-overview" skills="typescript-sdk" title="External Agents Overview" description="How to configure and use external agents using the A2A protocol">
  External agents let you integrate agents built outside of Inkeep (using other frameworks or platforms) into your Agent. They communicate over the A2A (Agent‑to‑Agent) protocol so your Inkeep sub-agents can delegate tasks to them as if they were native. Note that Inkeep Agents are available via an [A2A endpoint](/talk-to-your-agents/a2a) themselves and used from other platforms.

  Learn more about A2A:

  * A2A overview on the Google Developers Blog: [A2A — a new era of agent interoperability](https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/)
  * A2A protocol site: [a2a.how](https://a2a.how/)

  Examples platforms that expose Agents in A2A-format:

  | Platform                                                                                                                      | Type         | Description                                                         |
  | ----------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------- |
  | [LangGraph](https://docs.langchain.com/langgraph-platform/server-a2a)                                                         | Native       | Built-in A2A endpoint & Agent Card for graph agents.                |
  | [Google Agent Development Kit (ADK)](https://google.github.io/adk-docs/a2a/)                                                  | Native       | Official guide to build agents that expose/consume A2A.             |
  | [Microsoft Semantic Kernel](https://devblogs.microsoft.com/foundry/semantic-kernel-a2a-integration/)                          | Native       | “SK now speaks A2A” with sample to expose compliant agents.         |
  | [Pydantic AI](https://ai.pydantic.dev/a2a/)                                                                                   | Native       | Convenience method to publish a Pydantic AI agent as an A2A server. |
  | [AWS Strands Agents SDK](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/multi-agent/agent-to-agent/) | Native       | A2A support in Strands for cross‑platform agent communication.      |
  | [CrewAI](https://codelabs.developers.google.com/intro-a2a-purchasing-concierge)                                               | With Adapter | Use the A2A Python SDK to serve a CrewAI agent over A2A.            |
  | [LlamaIndex](https://a2aprotocol.ai/blog/a2a-samples-llama-index-file-chat-openrouter)                                        | With Adapter | Example Workflows app exposed via A2A (agent + card).               |

  <Note>
    Any agent that exposes an A2A‑compatible HTTP endpoint can be integrated by providing its `baseUrl` plus headers/auth (static or dynamic).
  </Note>

  ## Creating an External Agent

  Every external agent needs a unique identifier, name, description, base URL for A2A communication, and optional authentication configuration:

  ```typescript
  import { externalAgent } from "@inkeep/agents-sdk";

  const technicalSupportAgent = externalAgent({
    id: "technical-support-agent",
    name: "Technical Support Team",
    description: "External technical support specialists for complex issues",
    baseUrl: "https://api.example.com/agents/technical-support", // A2A endpoint
  });
  ```

  ## External Agent Relationships

  Agents can be configured to delegate tasks to external agents.

  ```typescript
  import { subAgent, agent } from "@inkeep/agents-sdk";
  import { myExternalAgent } from "./external-agents/exernal-agent-example";

  // Define the customer support sub-agent with delegation capabilities
  const supportSubAgent = subAgent({
    id: "support-agent",
    name: "Customer Support Sub-Agent",
    description: "Handles customer inquiries and escalates technical issues",
    prompt: `You are a customer support sub-agent that handles general customer inquiries.`,
    canDelegateTo: () => [myExternalAgent],
  });

  // Create the customer support agent with external agent capabilities
  export const supportAgent = agent({
    id: "customer-support-agent",
    name: "Customer Support System",
    description: "Handles customer inquiries and escalates to technical teams when needed",
    defaultSubAgent: supportSubAgent,
    subAgents: () => [supportSubAgent],
  });
  ```

  ## External Agent Options

  Configure authentication by providing a [credential reference](/typescript-sdk/credentials/overview).

  ```typescript
  const myExternalAgent = externalAgent({
    // Required
    id: "external-support-agent",
    name: "External Support Agent", // Human-readable agent name
    description: "External AI agent for specialized support", // Agent's purpose
    baseUrl: "https://api.example.com/agents/support", // A2A endpoint URL
    // Optional - Credential Reference
    credentialReference: myCredentialReference,
  });
  ```

  When delegating to an external agent, you can specify headers to include with every request to the external agent.
  These headers can be dynamic variables that are [resolved at runtime](/typescript-sdk/headers).

  ```typescript
  const supportSubAgent = subAgent({
    id: "support-agent",
    name: "Customer Support Sub-Agent",
    description: "Handles customer inquiries and escalates technical issues",
    prompt: `You are a customer support sub-agent that handles general customer inquiries.`,
    canDelegateTo: () => [myExternalAgent.with({ headers: { Authorization: "Bearer {{headers.Authorization}}" } })],
  });
  ```

  | Parameter             | Type                | Required | Description                                                                                                              |
  | --------------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
  | `id`                  | string              | Yes      | Stable agent identifier used for consistency and persistence                                                             |
  | `name`                | string              | Yes      | Human-readable name for the external agent                                                                               |
  | `description`         | string              | Yes      | Brief description of the agent's purpose and capabilities                                                                |
  | `baseUrl`             | string              | Yes      | The A2A endpoint URL where the external agent can be reached                                                             |
  | `credentialReference` | CredentialReference | No       | Reference to dynamic credentials for authentication. See [Credentials](/typescript-sdk/credentials/overview) for details |
</SkillRule>


# Headers in TypeScript SDK (/typescript-sdk/headers)

Define and validate HTTP headers in the TypeScript SDK to pass dynamic context for personalized agent interactions.



## Overview

Headers allow you to pass request-specific values (like user IDs, authentication tokens, or organization metadata) to your Agent at runtime via HTTP headers. These values are validated, cached per conversation, and made available throughout your Agent system for:

* **Context Fetchers**: Dynamic data retrieval based on request values
* **External Tools**: Authentication and personalization for API calls
* **Agent Prompts**: Personalized responses using context variables

<SkillRule id="headers-passing-context" skills="typescript-sdk" title="Passing Context via Headers" description="How to pass dynamic context to agents via HTTP headers">
  ## Passing context via headers

  Include context values as HTTP headers when calling your agent API. These headers are validated against your configured schema and cached for the conversation.

  ```bash
  curl -N \
    -X POST "http://localhost:3003/api/chat" \
    -H "Authorization: Bearer $INKEEP_API_KEY" \
    -H "user_id: u_123" \
    -H "auth_token: t_abc" \
    -H "org_name: Acme Corp" \
    -H "Content-Type: application/json" \
    -d '{
      "messages": [ { "role": "user", "content": "What can you help me with?" } ],
      "conversationId": "conv-123"
    }'
  ```

  <Note>
    Header keys are normalized to lowercase. Define them as lowercase in your schema and reference them as lowercase in templates.
  </Note>

  ## Configuring headers

  Define a schema for your headers and configure how it's used in your agent.
  You must include the headers schema in your context config.

  ```typescript
  import { z } from "zod";
  import { agent, subAgent } from "@inkeep/agents-sdk";
  import { contextConfig, fetchDefinition, headers } from '@inkeep/agents-core';


  // Define schema for expected headers (use lowercase keys)
  const personalAgentHeaders = headers({
    schema: z.object({
      user_id: z.string(),
      auth_token: z.string(),
      org_name: z.string().optional()
    });
  });

  // Create a context fetcher that uses header values with type-safe templating
  const userFetcher = fetchDefinition({
    id: "user-info",
    name: "User Information",
    trigger: "initialization",
    fetchConfig: {
      url: `https://api.example.com/users/${personalAgentHeaders.toTemplate('user_id')}`,
      method: "GET",
      headers: {
        Authorization: `Bearer ${personalAgentHeaders.toTemplate('auth_token')}`,
      },
      transform: "user", // Extract user from response, for example if the response is { "user": { "name": "John Doe", "email": "john.doe@example.com" } }, the transform will return the user object
    },
    responseSchema: z.object({
      user: z.object({
        name: z.string(),
        email: z.string(),
      }),
    }),
    defaultValue: "Guest User"
  });

  // Configure context for your agent
  const personalAgentContext = contextConfig({
    headers: personalAgentHeaders,
    contextVariables: {
      user: userFetcher,
    },
  });

  // Create a Sub Agent that uses context variables
  const personalAssistant = subAgent({
    id: "personal-assistant",
    name: "Personal Assistant",
    description: "Personalized AI assistant",
    prompt: `You are a helpful assistant for ${personalAgentContext.toTemplate('user.name')} from ${personalAgentHeaders.toTemplate('org_name')}.

    User ID: ${personalAgentHeaders.toTemplate('user_id')}

    Provide personalized assistance based on their context.`,
  });

  // Attach context to your Agent
  const myAgent = agent({
    id: "personal-agent",
    name: "Personal Assistant Agent",
    defaultSubAgent: personalAssistant,
    subAgents: () => [personalAssistant],
    contextConfig: personalAgentContext,
  });
  ```

  ## How it works

  <Steps>
    <Step>
      **Validation**: Headers are validated against your configured schema when a request arrives
    </Step>

    <Step>
      **Caching**: Validated context is cached per conversation for reuse across multiple interactions
    </Step>

    <Step>
      **Reuse**: Subsequent requests with the same `conversationId` automatically use cached context values
    </Step>

    <Step>
      **Updates**: Provide new header values to update the context for an ongoing conversation
    </Step>
  </Steps>

  <Tip>
    Context values persist across conversation turns. To update them, send new header values with the same conversation ID.
  </Tip>
</SkillRule>

<SkillRule id="headers-usage" skills="typescript-sdk" title="Using Headers in Agents" description="How to use header values in prompts, context fetchers, and external tools">
  ## Using headers in your agents

  Header values can be used in your agent prompts and fetch definitions using JSONPath template syntax `{{headers.field_name}}`.
  You can use the headers schema's `toTemplate()` method for type-safe templating with autocomplete and validation.

  ### In Context Fetchers

  Use header values to fetch dynamic data from external APIs:

  ```typescript
  // Define schema for expected headers (use lowercase keys)
  const personalAgentHeaders = headers({
    schema: z.object({
      user_id: z.string(),
      auth_token: z.string(),
      org_name: z.string().optional()
    });
  });


  const userDataFetcher = fetchDefinition({
    id: "user-data",
    name: "User Data",
    fetchConfig: {
      url: `https://api.example.com/users/${personalAgentHeaders.toTemplate('user_id')}/profile`,
      headers: {
        Authorization: `Bearer ${personalAgentHeaders.toTemplate('auth_token')}`,
        "X-Organization": personalAgentHeaders.toTemplate('org_name')
      },
      body: {
        includePreferences: true,
        userId: personalAgentHeaders.toTemplate('user_id')
      }
    },
    responseSchema: z.object({
      name: z.string(),
      preferences: z.record(z.unknown())
    })
  });

  // Configure context for your Agent
  // You must include the headers schema and fetchers in your context config.
  const personalAgentContext = contextConfig({
    headers: personalAgentHeaders,
    contextVariables: {
      user: userFetcher,
    },
  });
  ```

  ### In Agent Prompts

  Reference context directly in agent prompts for personalization using the context config's template method:

  ```typescript
  // Create context config with both headers and fetchers
  const userContext = contextConfig({
    headers: requestHeaders,
    contextVariables: {
      userName: userDataFetcher,
    },
  });

  const assistantAgent = subAgent({
    prompt: `You are an assistant for ${userContext.toTemplate('userName')} from ${requestHeaders.toTemplate('org_name')}.

    User context:
    - ID: ${requestHeaders.toTemplate('user_id')}
    - Organization: ${requestHeaders.toTemplate('org_name')}

    Provide help tailored to their organization's needs.`
  });
  ```

  ### In External Tools

  Configure external agents or MCP servers with dynamic headers using the headers schema:

  ```typescript
  // Define schema for expected headers (use lowercase keys)
  const personalAgentHeaders = headers({
    schema: z.object({
      user_id: z.string(),
      auth_token: z.string(),
      org_name: z.string().optional()
    });
  });

  // Configure external agent
  const externalAgent = externalAgent({
    id: "external-service",
    baseUrl: "https://external.api.com",
    headers: {
      Authorization: `Bearer ${personalAgentHeaders.toTemplate('auth_token')}`,
      "X-User-Context": personalAgentHeaders.toTemplate('user_id'),
      "X-Org": personalAgentHeaders.toTemplate('org_name')
    }
  });

  // Configure context for your Agent with your headers schema.
  const personalAgentContext = contextConfig({
    headers: personalAgentHeaders,
  });
  ```
</SkillRule>

## Verified claims from authenticated sessions

When using [authenticated chat sessions](/talk-to-your-agents/app-credentials#authenticated-chat-sessions), non-standard claims from the customer-signed JWT are automatically extracted and made available as **verified claims** in the conversation context. Unlike headers and `userProperties`, verified claims are cryptographically verified and cannot be modified by the client.

| Source              | Trust level                      | Mutability                |
| ------------------- | -------------------------------- | ------------------------- |
| **Headers**         | Validated against schema         | Updated per-request       |
| **User properties** | Unverified, client-provided      | Client can modify freely  |
| **Verified claims** | Cryptographically signed via JWT | Immutable for the session |

Verified claims are available at `metadata.verifiedClaims` in the execution context and are persisted in conversation metadata. Any non-standard claims in the JWT (e.g., `email`, `org_id`, `role`, or custom claims) are extracted automatically. Standard JWT claims (`iss`, `aud`, `exp`, `iat`, `nbf`, `jti`) are excluded.

<Note>
  Verified claims are limited to 1KB. Tokens with claims exceeding this limit are rejected.
</Note>

### Using verified claims in prompts

You can reference verified claims to personalize agent behavior with trusted identity data:

```typescript
const supportAgent = subAgent({
  id: "support-agent",
  name: "Support Agent",
  description: "Authenticated support assistant",
  prompt: `You are a support assistant.

  The user has been authenticated. Use their verified identity
  to provide personalized, account-specific assistance.

  Verified claims from their session are available in the conversation context
  and can be trusted for authorization decisions.`,
});
```

<Tip>
  Use verified claims for authorization and identity decisions. Use headers and `userProperties` for non-sensitive personalization like display preferences.
</Tip>

For setup instructions, see [Authenticated chat sessions](/talk-to-your-agents/app-credentials#authenticated-chat-sessions).

## Best practices

* **Use lowercase keys**: Always define schema properties in lowercase and reference them as lowercase in templates
* **Validate early**: Test your schema configuration with sample headers before deploying
* **Cache wisely**: Remember that context persists per conversation - design accordingly
* **Secure sensitive data**: For long-lived secrets, use the [Credentials](/typescript-sdk/credentials/overview) system instead of headers
* **Keep it minimal**: Only include context values that are actually needed by your agents

## Common use cases

### Multi-tenant applications

Pass tenant-specific configuration to customize agent behavior per customer:

```typescript
// Headers
"tenant_id: acme-corp"
"tenant_plan: enterprise"
"tenant_features: advanced-analytics,custom-branding"
```

### User authentication

Provide user identity and session information for personalized interactions:

```typescript
// Headers
"user_id: user_123"
"user_role: admin"
"session_token: sk_live_..."
```

### API gateway integration

Forward headers from your API gateway for consistent authentication:

```typescript
// Headers
"x-api-key: your-api-key"
"x-request-id: req_abc123"
"x-client-version: 2.0.0"
```

## Widget-provided headers

When using the [Inkeep chat components](/talk-to-your-agents), the following headers are automatically sent with each request:

| Header                      | Description                                       | Example Value              |
| --------------------------- | ------------------------------------------------- | -------------------------- |
| `x-inkeep-client-timestamp` | The client's current timestamp in ISO 8601 format | `2025-01-30T18:15:00.000Z` |
| `x-inkeep-client-timezone`  | The client's timezone identifier                  | `America/New_York`         |

These headers are useful for providing time-aware responses:

```typescript
const timeAwareHeaders = headers({
  schema: z.object({
    'x-inkeep-client-timestamp': z.string().optional(),
    'x-inkeep-client-timezone': z.string().optional(),
  }),
});

const timeAwareContext = contextConfig({
  headers: timeAwareHeaders,
});

const assistantAgent = subAgent({
  prompt: `You are a helpful assistant.

  The user's current time is: ${timeAwareHeaders.toTemplate('x-inkeep-client-timestamp')}
  The user's timezone is: ${timeAwareHeaders.toTemplate('x-inkeep-client-timezone')}

  Use this information to provide time-relevant responses when appropriate.`
});
```

## Troubleshooting

### Invalid headers errors

If you receive a 400 error about invalid headers:

1. Verify your schema matches the headers you're sending
2. Ensure all header keys are lowercase
3. Check that required fields are present
4. Validate the data types match your schema

### Context not persisting

If context values aren't available in subsequent requests:

1. Ensure you're using the same `conversationId` across requests
2. Verify headers are being sent correctly
3. Check that your context config is properly attached to the Agent

## Related documentation

* [Context Fetchers](/typescript-sdk/context-fetchers) - Learn about fetching and caching external data
* [External Agents](/typescript-sdk/external-agents) - Configure external agent integrations
* [Credentials](/typescript-sdk/credentials/overview) - Manage secure credentials for your Agents
* [App Credentials](/talk-to-your-agents/app-credentials) - Authentication for chat components, including authenticated sessions


# Conversation Memory (/typescript-sdk/memory)

Understand how conversation history is managed and included in the context window for both main and delegated agents



import { CompressionModelsTable } from '../../src/components/compression-models-table';

<SkillRule id="memory-overview" skills="typescript-sdk" title="Conversation Memory Overview" description="How conversation history is managed and included in context windows">
  ## Overview

  Conversation memory determines how much of the conversation history is included in the context window when your Agent processes a new message. The Inkeep Agent Framework automatically manages conversation history to balance context retention with token efficiency, with specialized handling for delegated agents and tool results.

  ## What's Included in Memory

  The conversation history now includes:

  * **Chat messages**: User messages and agent responses
  * **Tool results**: Results from tool executions, providing context about what actions were performed
  * **Agent communications**: Messages exchanged between agents during transfers and delegations
  * **Images**: User-uploaded images and images returned by tools are persisted and served via secure media proxy URLs

  ## Memory Management

  The system uses two approaches for managing conversation history:

  ### Intelligent Compression (Primary Method)

  When agents have a summarizer model configured (standard setup):

  * **Up to 10,000 messages**: Retrieves extensive conversation history to find compression summaries and make intelligent decisions
  * **No token limits**: Model-aware compression manages context based on each model's actual capabilities
  * **Dynamic optimization**: Automatically compresses when approaching model-specific thresholds (50% for conversation-level, 75-91% for sub-agent operations)

  ### Fixed Limits (Fallback Method)

  For agents without a summarizer model:

  * **50 messages**: Up to the 50 most recent messages from the conversation
  * **8,000 tokens**: Maximum of 8,000 tokens from previous conversation messages

  <Note>
    Most agents use intelligent compression, which provides superior context management tailored to each model's capabilities. The fixed limits serve as safety nets when a summarizer model is not available.
  </Note>

  ## Intelligent Compression System

  The framework's intelligent compression system is the primary method for managing conversation memory. It automatically analyzes model capabilities and compresses context when needed to optimize performance.

  ### How Compression Works

  The compression system operates continuously, making intelligent decisions about context management:

  <Steps>
    <Step>
      **Context Monitoring**: System continuously monitors conversation size against model limits
    </Step>

    <Step>
      **Automatic Triggering**: Compression triggers at 50% of context window for conversation-level, or at model-aware thresholds (\~75-91% depending on model size) for sub-agent generation
    </Step>

    <Step>
      **Tool Result Archiving**: Large tool results are stored as artifacts and replaced with summary references
    </Step>

    <Step>
      **AI Summarization**: Older conversation parts are summarized by AI while preserving key context
    </Step>

    <Step>
      **Fallback Protection**: If compression is unavailable, system falls back to fixed message and token limits
    </Step>
  </Steps>

  ### Model-Specific Behavior

  Different models have different context windows, and compression adapts accordingly:

  <CompressionModelsTable />

  ### Compression Types

  #### Conversation-Level Compression

  * **Trigger**: When conversation reaches 50% of model's context window
  * **Action**: Compresses entire conversation history into summary + artifacts
  * **Use Case**: Long conversations with extensive history

  **Example**: You have a 20-message conversation about planning a software project. The conversation includes requirements gathering, architecture discussions, and code reviews. When it hits the 50% threshold, the system creates a summary like "User discussed project requirements for e-commerce platform, decided on microservices architecture, reviewed authentication flow\..." and stores detailed tool outputs as artifacts.

  #### Sub-Agent Generation Compression

  * **Trigger**: During sub-agent execution when tool results exceed model-aware limits (75-91% depending on model size)
  * **Action**: Compresses generated tool results while preserving original context
  * **Use Case**: Sub-agents performing many tool operations during generation

  **Example**: A sub-agent is tasked with "analyze this codebase for security issues." During execution, it uses tools to:

  1. Read 15 different files (large outputs)
  2. Run security scans (detailed reports)
  3. Check dependencies (long lists)
  4. Analyze configurations (verbose JSON)

  When these tool results fill up the context window, the system compresses them into: "Analyzed 15 files, found 3 SQL injection risks in auth.py, 2 XSS vulnerabilities in templates..." while keeping the original conversation and task intact.

  <Note>
    Compression happens automatically and transparently. Your agents will continue to work normally even with compressed conversations, as the system preserves all essential context and provides artifact references for detailed information.
  </Note>

  ## How It Works

  <Steps>
    <Step>
      **Message Retrieval**: The system retrieves conversation history (up to 10,000 messages with intelligent compression, or 50 messages with fixed limits)
    </Step>

    <Step>
      **Delegation Filtering**: Messages are filtered based on delegation context - delegated agents see their own tool results plus top-level conversation context
    </Step>

    <Step>
      **Context Management**: With intelligent compression, the system analyzes model capabilities and compresses when needed. With fixed limits, messages are truncated at token thresholds.
    </Step>

    <Step>
      **Optimization**: Intelligent compression creates summaries and artifacts to preserve essential context while staying within model limits
    </Step>
  </Steps>

  ## Memory for Delegated Agents

  When agents delegate tasks to other agents, memory is intelligently filtered:

  ### Main Agents

  * See complete conversation history including all tool results
  * Maintain full context of delegated actions and their results

  ### Delegated Agents

  * See conversation history filtered to their delegation scope
  * Receive tool results from:
    * Their own tool executions
    * Top-level (non-delegated) tool executions
  * Cannot see tool results from unrelated delegations

  This ensures delegated agents have sufficient context while preventing memory pollution from unrelated parallel delegations.

  ## Tool Results in Memory

  Tool execution results are automatically included in conversation history, helping agents:

  * Understand what actions have already been performed
  * Avoid duplicate tool calls
  * Build on previous results when transferring between agents

  The tool results include both the input parameters and output results, formatted as:

  ```
  ## Tool: search_knowledge_base

  **Input:**
  {
    "query": "API authentication methods"
  }

  **Output:**
  {
    "results": [...]
  }
  ```

  ## Images in Memory

  User-uploaded images — both inline data URIs and external URLs — are automatically persisted to blob storage as part of the conversation history. Images included in tool results are also persisted. When conversation history is retrieved, blob references are resolved to secure media proxy URLs, ensuring images remain accessible across sessions without exposing internal storage paths.
</SkillRule>


# Model Configuration (/typescript-sdk/models)

Configure AI models for your Agents and Sub Agents



Configure models at **Project** (required), **Agent**, or **Sub Agent** levels. Settings inherit down the hierarchy.

## Configuration Hierarchy

You **must configure at least the base model** at the project level:

```typescript
// inkeep.config.ts
export default defineConfig({
  models: {
    base: {
      model: "anthropic/claude-sonnet-4-5",
      providerOptions: { temperature: 0.7, maxOutputTokens: 2048 }
    }
  }
});
```

Override at agent or sub agent level:

```typescript
const myAgent = agent({
  models: {
    base: { model: "openai/gpt-5.2" }  // Override project default
  }
});

const mySubAgent = subAgent({
  models: {
    structuredOutput: { model: "openai/gpt-4.1-mini" }  // Override for JSON output
  }
});
```

<SkillRule id="model-types" skills="typescript-sdk" title="Model Types Reference" description="When to use each model type in the configuration hierarchy">
  ## Model Types

  | Type               | Purpose                       | Fallback                      |
  | ------------------ | ----------------------------- | ----------------------------- |
  | `base`             | Text generation and reasoning | **Required at project level** |
  | `structuredOutput` | JSON/structured output only   | Falls back to `base`          |
  | `summarizer`       | Summaries and status updates  | Falls back to `base`          |

  Each model type inherits independently through the project → agent → sub agent hierarchy. `structuredOutput` and `summarizer` resolve to the most specific level that sets them, regardless of where `base` is configured — overriding only `base` at the agent or sub agent level does **not** change which `structuredOutput` or `summarizer` is used. They fall back to the resolved `base` only when no level sets them.
</SkillRule>

<SkillRule id="supported-models" skills="typescript-sdk" title="Supported Models Reference" description="List of supported model providers and their API keys">
  ## Supported Models

  | Provider                     | Example Models                                                                                                                                                                                                                                                      | API Key                        |
  | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
  | **Anthropic**                | `anthropic/claude-opus-4-8`<br />`anthropic/claude-opus-4-7`<br />`anthropic/claude-opus-4-6`<br />`anthropic/claude-sonnet-4-6`<br />`anthropic/claude-sonnet-4-5`<br />`anthropic/claude-haiku-4-5`                                                               | `ANTHROPIC_API_KEY`            |
  | **OpenAI**                   | `openai/gpt-5.5-pro`<br />`openai/gpt-5.5`<br />`openai/gpt-5.4-pro`<br />`openai/gpt-5.4`<br />`openai/gpt-5.4-mini`<br />`openai/gpt-5.4-nano`<br />`openai/gpt-5.2`<br />`openai/gpt-5.1`<br />`openai/gpt-4.1`<br />`openai/gpt-4.1-mini`<br />`openai/gpt-5`\* | `OPENAI_API_KEY`               |
  | **Azure OpenAI**             | `azure/my-gpt4-deployment`<br />`azure/my-gpt35-deployment`                                                                                                                                                                                                         | `AZURE_API_KEY`                |
  | **Google**                   | `google/gemini-3.1-pro-preview`<br />`google/gemini-3.5-flash`<br />`google/gemini-3.1-flash-lite`<br />`google/gemini-2.5-pro`<br />`google/gemini-2.5-flash`<br />`google/gemini-2.5-flash-lite`                                                                  | `GOOGLE_GENERATIVE_AI_API_KEY` |
  | **OpenRouter**               | `openrouter/anthropic/claude-sonnet-4-0`<br />`openrouter/meta-llama/llama-3.1-405b`                                                                                                                                                                                | `OPENROUTER_API_KEY`           |
  | **Gateway**                  | `gateway/openai/gpt-4.1-mini`                                                                                                                                                                                                                                       | `AI_GATEWAY_API_KEY`           |
  | **NVIDIA NIM**               | `nim/nvidia/llama-3.3-nemotron-super-49b-v1.5`<br />`nim/nvidia/nemotron-4-340b-instruct`                                                                                                                                                                           | `NIM_API_KEY`                  |
  | **Custom OpenAI-compatible** | `custom/my-custom-model`<br />`custom/llama-3-custom`                                                                                                                                                                                                               | `CUSTOM_LLM_API_KEY`           |
  | **Mock**                     | `mock/default`                                                                                                                                                                                                                                                      | None required                  |

  <Note>
    \*

    `openai/gpt-5`

    , 

    `openai/gpt-5-mini`

    , and 

    `openai/gpt-5-nano`

     require a verified OpenAI organization. If your organization is not yet verified, these models will not be available.
  </Note>

  ### Pinned vs Unpinned Models

  **Pinned models** include a specific date or version (e.g., `anthropic/claude-sonnet-4-20250514`) and always use that exact version.

  **Unpinned models** use generic identifiers (e.g., `anthropic/claude-sonnet-4-5`) and let the provider choose the latest version, which may change over time as providers update their models.

  ```typescript
  models: {
    base: {
      model: "anthropic/claude-sonnet-4-5",  // Unpinned - provider chooses version
      // vs
      model: "anthropic/claude-sonnet-4-20250514"  // Pinned - exact version
    }
  }
  ```

  The TypeScript SDK also provides constants for common models:

  ```typescript
  import { Models } from "@inkeep/agents-sdk";

  models: {
    base: {
      model: Models.ANTHROPIC_CLAUDE_SONNET_4_5,  // Type-safe constants
    }
  }
  ```
</SkillRule>

<SkillRule id="provider-options" skills="typescript-sdk" title="Model Provider Options" description="Configuration options for different model providers">
  ## Provider Options

  Inkeep Agents supports all [Vercel AI SDK provider options](https://ai-sdk.dev/providers/ai-sdk-providers/).

  ### How providerOptions works

  `providerOptions` accepts two types of values:

  * **Scalars** (`temperature`, `topP`, `maxOutputTokens`, `seed`, `maxDuration`) — standard generation parameters applied to every call
  * **Objects** (`anthropic: {}`, `openai: {}`, `gateway: {}`, etc.) — provider-specific options for that provider

  This means you can mix them freely:

  ```typescript
  providerOptions: {
    temperature: 0.7,            // generation param
    anthropic: {                 // Anthropic-specific options
      thinking: { type: 'enabled', budgetTokens: 8000 }
    }
  }
  ```

  <Note>
    Constructor-level config (`baseURL`, `headers`, `resourceName`, `apiVersion`) is always specified at the top level of `providerOptions`, not nested under a provider key.
  </Note>

  ### Complete Examples

  **Basic configuration:**

  <Tabs>
    <Tab title="TypeScript">
      ```typescript
      models: {
        base: {
          model: "anthropic/claude-sonnet-4-5",
          providerOptions: {
            maxOutputTokens: 4096,
            temperature: 0.7,
            topP: 0.95,
            seed: 12345,
            maxDuration: 30,
          }
        }
      }
      ```
    </Tab>

    <Tab title="JSON">
      ```json
      {
        "maxOutputTokens": 4096,
        "temperature": 0.7,
        "topP": 0.95,
        "seed": 12345,
        "maxDuration": 30
      }
      ```
    </Tab>
  </Tabs>

  **OpenAI with reasoning:**

  <Tabs>
    <Tab title="TypeScript">
      ```typescript
      models: {
        base: {
          model: "openai/o1-preview",
          providerOptions: {
            openai: { reasoningEffort: 'medium' }, // 'low' | 'medium' | 'high'
            maxOutputTokens: 4096
          }
        }
      }
      ```
    </Tab>

    <Tab title="JSON">
      ```json
      {
        "openai": { "reasoningEffort": "medium" },
        "maxOutputTokens": 4096
      }
      ```
    </Tab>
  </Tabs>

  **Anthropic with thinking:**

  <Tabs>
    <Tab title="TypeScript">
      ```typescript
      models: {
        base: {
          model: "anthropic/claude-sonnet-4-5",
          providerOptions: {
            anthropic: {
              thinking: { type: 'enabled', budgetTokens: 8000 }
            },
            temperature: 0.5
          }
        }
      }
      ```
    </Tab>

    <Tab title="JSON">
      ```json
      {
        "anthropic": {
          "thinking": { "type": "enabled", "budgetTokens": 8000 }
        },
        "temperature": 0.5
      }
      ```
    </Tab>
  </Tabs>

  **Google with thinking:**

  <Tabs>
    <Tab title="TypeScript">
      ```typescript
      models: {
        base: {
          model: "google/gemini-2.5-flash",
          providerOptions: {
            google: {
              thinkingConfig: { thinkingBudget: 8192, includeThoughts: true }
            },
            temperature: 0.7
          }
        }
      }
      ```
    </Tab>

    <Tab title="JSON">
      ```json
      {
        "google": {
          "thinkingConfig": { "thinkingBudget": 8192, "includeThoughts": true }
        },
        "temperature": 0.7
      }
      ```
    </Tab>
  </Tabs>

  **Controlling reasoning:**

  Each provider exposes reasoning through `providerOptions` (see the examples above):

  * **Anthropic** — `anthropic.thinking`
  * **OpenAI** — `openai.reasoningEffort`
  * **Google** — `google.thinkingConfig.thinkingBudget`

  Whether a model reasons **by default** varies by provider and changes as models are updated — check the provider's own reference: [Anthropic extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking), [OpenAI reasoning](https://developers.openai.com/api/docs/guides/reasoning), and [Google thinking](https://ai.google.dev/gemini-api/docs/thinking). As a rule of thumb, Anthropic and OpenAI models start with reasoning off (opt-in), while many Google models have it on by default (opt-out). For guidance on *when* to disable reasoning, see [Tuning Agents](/guides/agent-engineering/tuning-agents#control-reasoning).

  **Vercel AI Gateway:**

  When `AI_GATEWAY_API_KEY` is set, Anthropic, OpenAI, and Google models are automatically routed through [Vercel AI Gateway](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway) for per-request cost tracking. No configuration changes are needed — your existing model strings work as-is. See [AI Gateway](#ai-gateway) below for fallback models, allowed providers, and advanced gateway configuration.

  **Azure OpenAI:**

  <Tabs>
    <Tab title="TypeScript">
      ```typescript
      models: {
        base: {
          model: "azure/my-gpt4-deployment",
          providerOptions: {
            resourceName: "my-azure-openai-resource",  // Required
            apiVersion: "2024-10-21",  // Optional
            temperature: 0.7
          }
        }
      }
      ```
    </Tab>

    <Tab title="JSON">
      ```json
      {
        "resourceName": "my-azure-openai-resource",
        "apiVersion": "2024-10-21",
        "temperature": 0.7
      }
      ```
    </Tab>
  </Tabs>

  <Note>
    Azure OpenAI **requires** either `resourceName` (for standard Azure OpenAI deployments) or `baseURL` (for custom endpoints) in `providerOptions`. The `AZURE_API_KEY` environment variable must be set for authentication. Note that only one Azure OpenAI resource can be used at a time since authentication is handled via a single environment variable.
  </Note>

  **Custom OpenAI-compatible provider:**

  <Tabs>
    <Tab title="TypeScript">
      ```typescript
      models: {
        base: {
          model: "custom/my-custom-model",
          providerOptions: {
            baseURL: "https://api.example.com/v1",  // Required
            temperature: 0.7
          }
        }
      }
      ```
    </Tab>

    <Tab title="JSON">
      ```json
      {
        "baseUrl": "http://127.0.0.1:8090/v1",
        "temperature": 0.7
      }
      ```
    </Tab>
  </Tabs>

  <Note>
    Custom OpenAI-compatible providers **require** a base URL to be specified in `providerOptions.baseURL` or `providerOptions.baseUrl`. The `CUSTOM_LLM_API_KEY` environment variable will be automatically used for authentication if present.
  </Note>

  ### Context Window Override

  For custom or unlisted models, you can explicitly specify the context window size:

  <Tabs>
    <Tab title="TypeScript">
      ```typescript
      models: {
        base: {
          model: "custom/my-model",
          providerOptions: {
            contextWindowSize: 100000,  // Context window in tokens
            baseURL: "https://api.example.com/v1"
          }
        }
      }
      ```
    </Tab>

    <Tab title="JSON">
      ```json
      {
        "contextWindowSize": 100000,
        "baseUrl": "https://api.example.com/v1"
      }
      ```
    </Tab>
  </Tabs>

  <Note>
    The `contextWindowSize` option is useful when:

    * Using a custom model not in the built-in registry
    * The framework incorrectly detects the context window size
    * You want to artificially limit the context window for testing

    This affects compression triggers and oversized artifact detection (artifacts exceeding 30% of the context window).
  </Note>
</SkillRule>

## AI Gateway

[Vercel AI Gateway](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway) provides intelligent model routing, automatic failover, and per-request cost tracking. All AI Gateway features require `AI_GATEWAY_API_KEY` to be set in your environment.

When the gateway is active, **Anthropic**, **OpenAI**, and **Google** models are automatically routed through it — no configuration changes needed. This enables **fallback models** and **allowed providers** for those providers. **Per-request cost tracking** is available for any model routed through the gateway.

For self-hosted deployments, you can [bring your own API keys](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway#bring-your-own-key-byok) (BYOK) for any provider by configuring them in your Vercel AI Gateway team settings.

### Fallback Models

Configure an ordered list of backup models that the gateway tries if the primary model fails or is unavailable:

<Tabs>
  <Tab title="TypeScript">
    ```typescript
    models: {
      base: {
        model: "anthropic/claude-sonnet-4-5",
        fallbackModels: [
          "openai/gpt-5.2",
          "google/gemini-2.5-flash",
        ],
      }
    }
    ```
  </Tab>

  <Tab title="JSON">
    ```json
    {
      "model": "anthropic/claude-sonnet-4-5",
      "fallbackModels": [
        "openai/gpt-5.2",
        "google/gemini-2.5-flash"
      ]
    }
    ```
  </Tab>
</Tabs>

If the primary model (`anthropic/claude-sonnet-4-5`) fails, the gateway automatically tries `openai/gpt-5.2`, then `google/gemini-2.5-flash`. If all models fail, the request errors.

Fallback models can be configured on any model slot (`base`, `structuredOutput`, `summarizer`). They inherit through the project → agent → sub agent hierarchy — even when a sub agent overrides the model itself, it still inherits `fallbackModels` from its parent unless it explicitly sets its own. All entries must use the `provider/model` format (e.g., `"openai/gpt-5.2"`).

### Allowed Providers

Restrict and prioritize which providers can serve requests. The order determines preference — the first provider in the list has the highest priority:

<Tabs>
  <Tab title="TypeScript">
    ```typescript
    models: {
      base: {
        model: "claude-sonnet-4-5",
        allowedProviders: ["bedrock", "anthropic"],
      }
    }
    ```
  </Tab>

  <Tab title="JSON">
    ```json
    {
      "model": "claude-sonnet-4-5",
      "allowedProviders": ["bedrock", "anthropic"]
    }
    ```
  </Tab>
</Tabs>

The gateway will try to serve the request via AWS Bedrock first. If Bedrock is unavailable, it falls back to Anthropic's direct API. When `allowedProviders` is set, the model string should omit the provider prefix (e.g., `"claude-sonnet-4-5"` instead of `"anthropic/claude-sonnet-4-5"`) since the gateway determines which provider to use.

Available providers include: `anthropic`, `openai`, `google`, `bedrock`, `azure`, `vertex`.

Like `fallbackModels`, `allowedProviders` inherits through the project → agent → sub agent hierarchy even when the model is overridden at a lower level.

### Combining Both

Both features can be used together. With this configuration, `allowedProviders` restricts and prioritizes which providers can serve the primary model, while `fallbackModels` defines alternative models to try if the primary model is unavailable:

```typescript
models: {
  base: {
    model: "claude-sonnet-4-5",
    allowedProviders: ["bedrock", "anthropic"],
    fallbackModels: ["openai/gpt-5.2", "google/gemini-2.5-flash"],
  }
}
```

### Advanced: Manual Gateway Provider Options

You can also configure gateway routing directly via `providerOptions` for access to the full [gateway provider options](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway#gateway-provider-options) (`order`, `only`, `models`, `byok`, `providerTimeouts`, `tags`, `zeroDataRetention`).

<Note>
  `providerOptions.gateway.caching` and `providerOptions.anthropic.cacheControl` interact with Inkeep's default-on [prompt caching](/guides/observability/prompt-caching). Inkeep applies its caching default only when you haven't set a value — if you set `providerOptions.gateway.caching` or `providerOptions.anthropic.cacheControl` explicitly, your value is preserved.
</Note>

```typescript
models: {
  base: {
    model: "gateway/openai/gpt-4.1",
    providerOptions: {
      gateway: {
        models: ["openai/gpt-4.1", "anthropic/claude-sonnet-4-5"],
      }
    }
  }
}
```

<Note>
  If you set both `fallbackModels` and `providerOptions.gateway.models`, `fallbackModels` takes precedence.
</Note>

## CLI Defaults

When using `inkeep init`, defaults are set based on your chosen provider:

| Provider      | Base                | Structured Output       | Summarizer              |
| ------------- | ------------------- | ----------------------- | ----------------------- |
| **Anthropic** | `claude-sonnet-4-5` | `claude-sonnet-4-5`     | `claude-sonnet-4-5`     |
| **OpenAI**    | `gpt-5.2`           | `gpt-4.1-mini`          | `gpt-5.4-nano`          |
| **Google**    | `gemini-2.5-flash`  | `gemini-2.5-flash-lite` | `gemini-2.5-flash-lite` |


# Project Management (/typescript-sdk/project-management)

Learn how to manage projects in the Inkeep Agent Framework



<>
  ## Overview

  Projects are the top-level organizational unit in your Inkeep workspace. Each project contains its own agents, sub agents, tools, data components, and environment configurations.

  ## Workspace structure

  After following the [Quick Start](/get-started/quick-start) guide, your workspace will be organized with the following directory structure:

  ```
  workspace-root/                 
  ├── package.json                 # Workspace package.json
  ├── .env                         # Workspace environment variables
  ├── src/  
  │   ├── inkeep.config.ts         # Workspace configuration file
  │   ├── projects/
  │   │   ├── my-agent-project/
  │   │   │   ├── index.ts
  │   │   │   ├── agents/
  │   │   │   │   ├── main-agent.ts
  │   │   │   │   └── support-agent.ts
  │   │   │   ├── tools/
  │   │   │   │   ├── search-tool.ts
  │   │   │   │   └── calculator-tool.ts
  │   │   │   ├── data-components/
  │   │   │   │   ├── user-profile.ts
  │   │   │   │   └── product-catalog.ts
  │   │   │   ├── external-agents/
  │   │   │   │   ├── exernal-agent-example.ts
  │   │   │   └── environments/
  │   │   │       ├── index.ts
  │   │   │       ├── development.env.ts
  │   │   │       └── production.env.ts
  │   │   └── another-project/       # Additional projects can coexist
  │   │       ├── index.ts
  │   │       └── ...
  ```

  The `src/inkeep.config.ts` file defines workspace configuration for all projects. See [Workspace Configuration](/typescript-sdk/workspace-configuration) for details.

  ## Architecture

  This project follows a workspace structure with the following services:

  * **Agents Manage UI** (Port 3000): Web interface
    * The agent framework visual builder. From the builder you can create, manage and visualize all your graphs.
  * **Agents API** (Port 3002): Agent configuration and management
    * Handles entity management and configuration endpoints.
    * Handles agent communication. You can interact with your agents either over MCP from an MCP client or through our React UI components library
  * **MCP Service** (Port 3006): Custom MCP servers
    * Custom MCP servers for external tools and APIs. See [our guide](https://docs.inkeep.com/tutorials/mcp-servers/custom-mcp-servers#getting-started) for how to add MCP servers to your project.

  ## Project files and directories

  ### `index.ts`

  The project entry point inside each project directory that exports your project definition:

  ```typescript file="src/projects/my-agent-project/index.ts"
  // Located inside project directory (e.g., my-agent-project/index.ts)
  import { project } from '@inkeep/agents-sdk';
  import { mainAgent } from './agents/main-agent';
  import { supportAgent } from './agents/support-agent';
  import { searchTool } from './tools/search-tool';
  import { calculatorTool } from './tools/calculator-tool';
  import { userProfile } from './data-components/user-profile';

  export const myProject = project({
    id: 'my-agent-project',
    name: 'My Agent Project',
    description: 'A comprehensive multi-agent system',
    subAgents: () => [mainAgent, supportAgent],
    tools: () => [searchTool, calculatorTool],
    dataComponents: () => [userProfile],
  });
  ```

  ### `agents/`

  Contains agent definitions. Each file typically exports one agent:

  ```typescript file="src/projects/my-agent-project/agents/customer-support.ts"
  // agents/customer-support.ts
  import { agent, subAgent } from '@inkeep/agents-sdk';

  const routerSubAgent = subAgent({
    id: 'support-router',
    name: 'Support Router',
    prompt: 'Route customer inquiries to appropriate specialists',
  });

  const billingSubAgent = subAgent({
    id: 'billing-specialist',
    name: 'Billing Specialist',
    prompt: 'Handle billing and payment inquiries',
  });

  export const customerSupportAgent = agent({
    defaultSubAgent: routerSubAgent,
    subAgents: () => [routerSubAgent, billingSubAgent],
  });
  ```

  See [Agents & Sub Agents](https://docs.inkeep.com/typescript-sdk/agent-settings) for more information about creating and configuring agents and sub agents.

  ### `tools/`

  Tool definitions that can be used by Sub Agents:

  ```typescript
  // tools/database-query.ts
  import { mcpTool } from '@inkeep/agents-sdk';

  export const databaseQueryTool = mcpTool({
    id: 'db-query',
    name: 'Database Query Tool',
    description: 'Execute SQL queries against the database',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string' },
        database: { type: 'string' }
      }
    },
    // Tool implementation...
  });
  ```

  See [MCP Tools](https://docs.inkeep.com/typescript-sdk/tools/mcp-tools) and [Function Tools](https://docs.inkeep.com/typescript-sdk/tools/function-tools) for more information about creating and configuring MCP tools and function tools.

  ### `data-components/`

  Data components for structured UI output:

  ```typescript
  // data-components/customer-data.ts
  import { dataComponent } from '@inkeep/agents-sdk';
  import { z } from 'zod';

  export const customerData = dataComponent({
    id: 'customer-data',
    name: 'Customer Information',
    description: 'Customer profile and interaction history',
    props: z.object({
      customerId: z.string().describe("Customer ID"),
      name: z.string().describe("Customer name"),
      email: z.string().describe("Customer email"),
    }),
  });
  ```

  See [Data Components](https://docs.inkeep.com/typescript-sdk/structured-outputs/data-components) for more information about creating and configuring data components.

  ### `external-agents/`

  External agent definitions:

  ```typescript
  import { externalAgent } from '@inkeep/agents-sdk';

  export const exernalAgentExample = externalAgent({
    id: 'exernal-agent-example',
    name: 'Exernal Agent Example',
    description: 'An example external agent',
    baseUrl: 'https://api.example.com/agents/support',
    credentialReference: myCredentialReference,
  });
  ```

  See [External Agents](https://docs.inkeep.com/typescript-sdk/external-agents) for more information about creating and configuring external agents.

  ### `environments/`

  Environment-specific configurations for different deployment stages:

  ```typescript
  // environments/production.env.ts
  import { registerEnvironmentSettings } from '@inkeep/agents-sdk';
  import { CredentialStoreType } from '@inkeep/agents-core';

  export const production = registerEnvironmentSettings({
    credentials: {
      "openai-prod": {
        id: "openai-prod",
        type: CredentialStoreType.memory,
        credentialStoreId: "memory-default",
        retrievalParams: {
          key: "OPENAI_API_KEY_PROD",
        },
      },
    },
  });
  ```

  ## How to add a new project

  Use the `inkeep add` command to add template projects from the [Inkeep Agents Cookbook](https://github.com/inkeep/agents/tree/main/agents-cookbook/template-projects) to your workspace.

  ### Prerequisite

  Ensure you have the [Inkeep CLI](/typescript-sdk/cli-reference) installed.

  ```bash
  npm install -g @inkeep/agents-cli
  ```

  ### Step 1: Navigate to your projects directory

  Navigate to the `src/projects` directory:

  ```bash
  cd src/projects
  ```

  ### Step 2: List available templates

  To see all available project templates, run `inkeep add` without any arguments:

  ```bash
  inkeep add
  ```

  This displays a list of available project templates you can add to your workspace.

  ### Step 3: Add a project template

  Add a specific project template using the `--project` flag:

  ```bash
  inkeep add --project docs-assistant
  ```

  For new projects, we recommend starting with a simple project template like docs-assistant, which you can customize to your needs.

  ## Pushing changes to the Visual Builder

  After making changes to your project code, use `inkeep push` to sync your local TypeScript project to the Visual Builder. This allows you to continue development using the Visual Builder's drag-and-drop interface.

  ### Prerequisite

  Ensure you have the [Inkeep CLI](/typescript-sdk/cli-reference) installed.

  ```bash
  npm install -g @inkeep/agents-cli
  ```

  ### Step 1: Navigate to your project directory

  Change to the directory containing your project's `index.ts` file:

  ```bash
  cd src/projects/my-agent-project
  ```

  ### Step 2: Push your project

  Run the push command:

  ```bash
  inkeep push
  ```

  ### Step 3: Verify in Visual Builder

  After pushing, refresh your Visual Builder. Your project and all its agents, Sub Agents, tools, and data components will be available in the Visual Builder.

  ### Additional options

  **Validate without pushing:**

  To validate your project without actually pushing changes, use the `--json` flag:

  ```bash
  inkeep push --json
  ```

  ## Pulling changes from the Visual Builder

  When you make changes in the Visual Builder (such as updating prompts or modifying agent configurations), use `inkeep pull` to sync those changes back to your local TypeScript project.

  ### Prerequisite

  The `inkeep pull` command in-part leverages AI to sync your TypeScript files to the state of your Visual Builder, so **at least one** of the below environment variables to be defined:

  ```txt .env
  # Choose one:
  ANTHROPIC_API_KEY=your_api_key_here
  # or
  OPENAI_API_KEY=your_api_key_here
  # or
  GOOGLE_API_KEY=your_api_key_here
  ```

  The CLI prioritizes Anthropic → OpenAI → Google.

  Here are the models used:

  | Provider  | Model(s)                              | Where to Get API Key                                |
  | --------- | ------------------------------------- | --------------------------------------------------- |
  | Anthropic | Claude Sonnet 4.5 (extended thinking) | [Anthropic Console](https://console.anthropic.com/) |
  | OpenAI    | GPT-5.1                               | [OpenAI Platform](https://platform.openai.com/)     |
  | Google    | Gemini 2.5 Flash                      | [Google AI Studio](https://ai.google.dev/)          |

  ### Step 1: Navigate to your project directory

  Change to the directory containing your project's `index.ts` file:

  ```bash
  cd src/projects/my-agent-project
  ```

  ### Step 2: Pull changes from Visual Builder

  Run the pull command:

  ```bash
  inkeep pull
  ```

  ### Step 3: Verify the changes

  Check your project files to see the updates. For example, if you changed an agent's prompt in the Visual Builder:

  ```bash
  cat agents/my-agent.ts
  ```

  The file should reflect the updated prompt from the Visual Builder.

  See [CLI Reference](/typescript-sdk/cli-reference#inkeep-pull) for more information about the `inkeep pull` command.

  ## How to add a new MCP server

  Use the `inkeep add` command to add custom MCP server templates from the [Inkeep Agents Cookbook](https://github.com/inkeep/agents/tree/main/agents-cookbook/template-mcps) to your workspace. These templates provide starter code for common MCP server integrations that you can customize and deploy.

  ### Prerequisite

  Ensure you have the [Inkeep CLI](/typescript-sdk/cli-reference) installed.

  ```bash
  npm install -g @inkeep/agents-cli
  ```

  ### Step 1: Navigate to your workspace root

  Navigate to your workspace root directory (where your `package.json` is located):

  ```bash
  cd /path/to/your/workspace
  ```

  The CLI will automatically detect the `apps/mcp/app` directory in your workspace.

  ### Step 2: List available MCP templates

  To see all available MCP server templates, run `inkeep add` without any arguments:

  ```bash
  inkeep add
  ```

  This displays both project templates and MCP server templates you can add to your workspace.

  ### Step 3: Add an MCP server template

  Add a specific MCP server template using the `--mcp` flag:

  ```bash
  inkeep add --mcp zendesk
  ```

  The template will be added to `apps/mcp/app/[template-name]/mcp/route.ts` automatically.

  ### Step 4: Customize and register

  The MCP server will be exposed at `http://localhost:3000/[template-name]/mcp`. Customize it with your API credentials and business logic, then register it as a tool using `mcpTool`:

  ```typescript
  import { mcpTool } from '@inkeep/agents-sdk';

  export const zendeskTool = mcpTool({
    id: 'zendesk',
    name: 'zendesk',
    description: 'Zendesk API',
    serverUrl: 'http://localhost:3000/zendesk/mcp',
  });
  ```

  See [MCP Tools](/typescript-sdk/tools/mcp-tools) for more information about registering MCP servers as tools.

  ## Deployment Guides

  To build and run your own images you can follow the [Build a Custom Docker Image](https://docs.inkeep.com/deployment/docker-build) docs.

  This repostory contains a `docker-compose.yml` and a template `Dockerfile` for each service:

  * `Dockerfile.agents-api`
  * `Dockerfile.agents-manage-ui`
  * `Dockerfile.agents-migrate`

  To build and run:

  ```bash
  docker compose build
  docker compose up -d
  ```

  ### Deploy using official prebuilt images

  * [Deploy to Vercel](https://docs.inkeep.com/deployment/vercel)
  * [Docker (Local Dev)](https://docs.inkeep.com/deployment/docker-local)
  * [AWS EC2](https://docs.inkeep.com/deployment/aws-ec2)
  * [Azure VM](https://docs.inkeep.com/deployment/azure-vm)
  * [GCP Compute Engine](https://docs.inkeep.com/deployment/gcp-compute-engine)
  * [Hetzner](https://docs.inkeep.com/deployment/hetzner)
</>


# Prompt template variables (/typescript-sdk/prompt-template-variables)

Reference for the template variables you can use inside agent prompts — contextVariable, headers, and $conversation.



## Overview

Agent prompts support three template variable families. Each is rendered into the system prompt before the LLM sees it, so the model receives concrete values and can pass them through to tool calls as plain strings.

| Variable         | Syntax                              | Resolves to                           | Details                                              |
| ---------------- | ----------------------------------- | ------------------------------------- | ---------------------------------------------------- |
| Context variable | `{{contextVariable.<name>.<path>}}` | Value from a context fetcher          | [Context Fetchers](/typescript-sdk/context-fetchers) |
| Header           | `{{headers.<name>}}`                | Value from a validated request header | [Headers](/typescript-sdk/headers)                   |
| Conversation ID  | `{{$conversation.id}}`              | The current conversation's ID         | [Below](#conversation-id)                            |

<Note>
  Template variables only resolve inside **agent prompts**. The same `{{...}}` syntax appears in some other templated fields (context fetcher URLs, MCP credential templates, team agent delegation headers), but those fields only resolve `{{contextVariable.*}}` and `{{headers.*}}`.
</Note>

## Context variables and headers

See [Context Fetchers](/typescript-sdk/context-fetchers) and [Headers](/typescript-sdk/headers) for full coverage — how to define them, how their schemas work, and how `toTemplate()` gives you type-safe references with autocomplete.

## Conversation ID

`{{$conversation.id}}` resolves to the current conversation's ID at prompt-render time. Works in any agent prompt — no `contextConfig` required.

### When to use it

When you want the LLM to pass the conversation ID into tool arguments. Typical use: writing to an external system (ticketing, CRM, analytics) where you want a correlation key that links the external record back to the conversation.

### Example — Zendesk ticket correlation

```typescript
const zendeskAgent = subAgent({
  id: 'zendesk-agent',
  name: 'Zendesk Support Agent',
  description: 'Creates Zendesk tickets with conversation correlation',
  prompt: `You are a support agent with access to Zendesk.
    When creating a Zendesk ticket, set the "inkeep_conversation" custom field
    to {{$conversation.id}} so support engineers can trace the ticket back to
    this conversation.`,
  canUse: () => [zendeskMcpTool.with({ selectedTools: ['create_zendesk_ticket'] })],
});
```

See the [customer-support cookbook template](https://github.com/inkeep/agents/tree/main/agents-cookbook/template-projects/customer-support) for a complete working example.

### Resolution behavior

| Situation                                                        | Rendered value                                  |
| ---------------------------------------------------------------- | ----------------------------------------------- |
| Request has a conversation ID                                    | The conversation ID string (e.g. `conv_abc123`) |
| Request has no conversation context                              | Empty string                                    |
| Conversation ID is the framework's `'default'` fallback sentinel | Empty string                                    |

### Delegation and transfer

When an agent delegates to a sub-agent via A2A or transfers control, the sub-agent's `{{$conversation.id}}` resolves to **the parent's conversation ID** — the user-initiated one. This ensures records written by delegated sub-agents correlate back to the conversation the user started.


# Skills in TypeScript SDK (/typescript-sdk/skills)

Attach reusable instruction blocks to sub-agents



## Overview

Skills are reusable instruction blocks you can attach to sub-agents. They help you share behavioral rules, reasoning guidelines, and formatting standards across multiple sub-agents without duplicating prompt content.

Use skills to:

* **Standardize behavior** across sub-agents (tone, formatting, guardrails)
* **Separate concerns** by moving specialized instructions out of the main prompt
* **Load instructions on demand** to reduce context size when not needed

<Tip>
  For the Visual Builder approach, see [Skills in the Visual Builder](/visual-builder/skills).
</Tip>

## Defining Skills

Skills are defined as `SKILL.md` files with YAML frontmatter. Each skill lives in its own directory.
Any additional files under that skill directory are loaded and synced alongside `SKILL.md`.

### File Structure

```
skills/
├── weather-safety-guardrails/
│   ├── SKILL.md
│   └── reference/
│       └── safety-checklist.txt
└── structured-itinerary-responses/
    ├── SKILL.md
    └── templates/
        └── day/
            └── itinerary-card.html
```

<Note>
  The directory name must match the 

  `name`

   field in the frontmatter.
</Note>

### SKILL.md Format

```markdown title="skills/weather-safety-guardrails/SKILL.md"
---
name: "weather-safety-guardrails"
description: "Safety rules for weather-dependent activity recommendations"
metadata:
  author: acme-corp
  version: "1.0"
---

- Never recommend outdoor activities during severe weather alerts.
- Always mention indoor alternatives when precipitation exceeds 50%.
- Include safety disclaimers for extreme temperature conditions.
```

### Frontmatter Fields

| Field         | Type   | Required | Description                                                                    |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------ |
| `name`        | string | Yes      | Skill identifier. Must match the parent directory name.                        |
| `description` | string | Yes      | When to use this skill. Used by the agent to decide whether to load the skill. |
| `metadata`    | object | No       | Additional key-value pairs (author, version, tags, etc.).                      |

The body content after the frontmatter contains the actual instructions in Markdown format.

<Tip>
  The `description` is how the agent decides whether to load an on-demand skill, so make it specific and front-load the scenarios it applies to ("Use when…"). Keep it tight: descriptions are capped at **1024 characters**, and every always-loaded skill — plus the outline of each on-demand skill — sits in the system prompt on every turn, so verbose descriptions cost context. See [Tuning Agents](/guides/agent-engineering/tuning-agents#keep-tool-and-skill-descriptions-tight).
</Tip>

## Loading Skills

Use `loadSkills()` to load all skills from a directory:

```typescript
import path from 'node:path';
import { loadSkills, project } from '@inkeep/agents-sdk';
import { myAgent } from './agents/my-agent';

export const myProject = project({
  id: 'my-project',
  name: 'My Project',
  agents: () => [myAgent],
  skills: () => loadSkills(path.join('my-project/skills')),
  models: {
    base: {
      model: 'openai/gpt-4o',
    },
  },
});
```

The `loadSkills()` function:

1. Scans the directory for `*/SKILL.md` files
2. Parses each file's frontmatter and content
3. Recursively loads every file in each skill directory with paths relative to the skill root
4. Validates that the `name` matches the directory name
5. Returns an array of `SkillDefinition` objects

When you run `inkeep push`, the CLI syncs the full skill directory, including nested files. `SKILL.md` still provides the top-level skill name, description, metadata, and prompt content.

## Attaching Skills to Sub-Agents

Add skills to a sub-agent using the `skills` configuration option.

### Reference by ID

Reference skills defined at the project level by their ID:

```typescript
import { subAgent } from '@inkeep/agents-sdk';

const activitiesPlanner = subAgent({
  id: 'activities-planner',
  name: 'Activities Planner',
  description: 'Plans activities based on weather conditions',
  prompt: 'Help users plan activities based on current weather.',
  skills: () => [
    { id: 'weather-safety-guardrails', index: 0 },
    { id: 'structured-itinerary-responses', index: 1 },
  ],
});
```

### Skill Reference Options

| Field          | Type    | Required | Description                                                                          |
| -------------- | ------- | -------- | ------------------------------------------------------------------------------------ |
| `id`           | string  | Yes      | The skill identifier (matches the `name` in SKILL.md).                               |
| `index`        | number  | No       | Controls ordering. Higher index = higher priority in the prompt.                     |
| `alwaysLoaded` | boolean | No       | If `true`, skill content is always included. If `false` (default), loaded on demand. |

### Attaching Skills

Define attached skills to subagents:

```typescript
const mySubAgent = subAgent({
  id: 'my-sub-agent',
  name: 'My Sub Agent',
  prompt: 'You are a helpful assistant.',
  skills: () => [
    {
      id: 'inline-skill',
      index: 0,
      alwaysLoaded: true,
    },
  ],
});
```

## Always Loaded vs On-Demand Skills

Skills can be loaded in two ways:

### Always Loaded

Set `alwaysLoaded: true` to include the skill content in every prompt:

```typescript
skills: () => [
  { id: 'brand-guidelines', alwaysLoaded: true },
]
```

Use for skills that apply to every interaction (brand voice, compliance rules).

### On-Demand (Default)

When `alwaysLoaded` is `false` or omitted, skills appear as an outline in the system prompt. The agent can load the full content and any synced nested files when needed using the built-in `load_skill` tool.

```typescript
skills: () => [
  { id: 'technical-troubleshooting' }, // Loaded on demand
]
```

Use for specialized instructions that only apply to certain requests.

## Example

A complete example with skills directory structure and usage:

```
activities-planner-advanced/
├── index.ts
├── agents/
│   └── activities-planner-advanced.ts
└── skills/
    ├── weather-safety-guardrails/
    │   ├── SKILL.md
    │   └── reference/
    │       └── safety-checklist.txt
    └── structured-itinerary-responses/
        ├── SKILL.md
        └── templates/
            └── day/
                └── itinerary-card.html
```

```typescript title="index.ts"
import path from 'node:path';
import { loadSkills, project } from '@inkeep/agents-sdk';
import { activitiesPlannerAgent } from './agents/activities-planner-advanced';

export const activitiesPlannerAdvanced = project({
  id: 'activities-planner-advanced',
  name: 'Activities Planner Advanced',
  description: 'Plans activities based on weather forecasts',
  agents: () => [activitiesPlannerAgent],
  skills: () => loadSkills(path.join('activities-planner-advanced/skills')),
  models: {
    base: {
      model: 'openai/gpt-4o-mini',
    },
  },
});
```

```typescript title="agents/activities-planner-advanced.ts"
import { subAgent, agent } from '@inkeep/agents-sdk';

const activitiesPlanner = subAgent({
  id: 'activities-planner',
  name: 'Activities Planner',
  description: 'Plans activities considering weather conditions',
  prompt: 'Help users find activities based on weather forecasts.',
  skills: () => [
    { id: 'weather-safety-guardrails', index: 0 },
    { id: 'structured-itinerary-responses', index: 1 },
  ],
});

export const activitiesPlannerAgent = agent({
  id: 'activities-planner-advanced',
  name: 'Activities Planner Advanced',
  defaultSubAgent: activitiesPlanner,
  subAgents: () => [activitiesPlanner],
});
```

```markdown title="skills/weather-safety-guardrails/SKILL.md"
---
name: "weather-safety-guardrails"
description: "Safety rules for weather-dependent recommendations"
metadata:
  author: acme-corp
  version: "1.0"
---

- Never recommend outdoor activities during severe weather alerts.
- Always mention indoor alternatives when precipitation exceeds 50%.
- Include safety disclaimers for extreme temperature conditions.
```

```markdown title="skills/structured-itinerary-responses/SKILL.md"
---
name: "structured-itinerary-responses"
description: "Present time-aware itineraries with clear actions and citations"
metadata:
  author: acme-corp
  version: "1.0"
---

- Start with a one-sentence summary tailored to the user's location and weather window.
- Provide 3–5 time-blocked suggestions with clear actions, durations, and required items.
- End with a concise checklist plus source references when web results influence recommendations.
```

## Related

* [Skills in the Visual Builder](/visual-builder/skills)
* [Sub Agent Relationships](/typescript-sdk/agent-relationships)
* [Project Management](/typescript-sdk/project-management)


# Workspace Configuration (/typescript-sdk/workspace-configuration)

Learn how to configure your workspace





<SkillRule id="workspace-config-overview" skills="typescript-sdk" title="Workspace Configuration File" description="Structure and options for inkeep.config.ts">
  ## Overview

  The `inkeep.config.ts` file at the workspace root defines settings for all projects in this workspace. See [Project Management](/typescript-sdk/project-management) for where this file should be placed.

  ```typescript
  import { defineConfig } from "@inkeep/agents-cli/config";
  import "dotenv/config";

  export default defineConfig({
    tenantId: 'my-company',
    agentsApi: {
      url: 'http://localhost:3002',
      apiKey: process.env.API_KEY, // Optional
    },
    outputDirectory: "./output",
  });
  ```

  <AutoTypeTable name="default" type="export { NestedInkeepConfig as default } from '@inkeep/agents-cli/config'" />

  <AutoTypeTable name="default" type="export { ApiConfig as default } from '@inkeep/agents-cli/config'" />
</SkillRule>

<SkillRule id="config-hierarchy" skills="typescript-sdk" title="Configuration Priority Hierarchy" description="How CLI flags, env vars, and config file values are resolved">
  ## Configuration hierarchy

  One can override the settings in `inkeep.config.ts` by setting the following settings in this order (highest to lowest priority):

  <Mermaid chart="graph LR\n    A[CLI flags] --> B[Environment variables]\n    B --> C[Config file values]\n    C --> D[Built-in defaults]\n\n    style A fill:\n    style B fill:\n    style C fill:\n    style D fill:" />

  ### 1. CLI Flags

  Command-line flags override all other settings:

  ```bash
  # Override API URL
  inkeep push --agents-api-url https://api.production.com

  # Override config file location
  inkeep pull --config /path/to/custom.config.ts
  ```

  ### 2. Environment Variables

  Environment variables override config file values:

  ```bash
  # Set via environment
  export INKEEP_TENANT_ID=staging-tenant
  export INKEEP_AGENTS_API_URL=https://api.staging.com

  # Now CLI commands use these values
  inkeep push
  ```

  **Supported Environment Variables:**

  | Variable                | Config Equivalent | Description       |
  | ----------------------- | ----------------- | ----------------- |
  | `INKEEP_TENANT_ID`      | `tenantId`        | Tenant identifier |
  | `INKEEP_AGENTS_API_URL` | `agentsApi.url`   | Agents API URL    |
</SkillRule>

### 3. Config File Values

Values explicitly set in your `inkeep.config.ts`:

```typescript
export default defineConfig({
  tenantId: 'my-tenant',
  agentsApi: {
    url: 'http://localhost:3002',
  },
});
```

### 4. Built-in Defaults

Default values used when not specified elsewhere:

```typescript
const defaults = {
  agentsApiUrl: 'http://localhost:3002',
  manageUiUrl: 'http://localhost:3000',
};
```

## Working with multiple configurations

### Dynamic configuration

You can use environment-based logic in your workspace config:

```typescript
// inkeep.config.ts
import { defineConfig } from "@inkeep/agents-cli/config";

const isDevelopment = process.env.NODE_ENV === "development";

export default defineConfig({
  tenantId: process.env.TENANT_ID || 'default-tenant',
  agentsApi: {
    url: isDevelopment
      ? 'http://localhost:3002'
      : 'https://api.production.com',
  },
});
```

### Multiple configuration files

For workspaces requiring different configurations:

```typescript
// inkeep.config.ts
export default defineConfig({
  tenantId: 'production-tenant',
  agentsApi: {
    url: 'https://api.production.com',
  },
});
```

```typescript
// inkeep.dev.config.ts
export default defineConfig({
  tenantId: 'dev-tenant',
  agentsApi: {
    url: 'http://localhost:3002',
  },
});
```

```bash
# Use development config (specify from any project directory)
inkeep push --config ../inkeep.dev.config.ts
# or with absolute path
inkeep push --config /path/to/workspace/inkeep.dev.config.ts
```


# Access Control (/visual-builder/access-control)

Multi-tenant authentication with organizations, team management, and fine-grained project permissions



The Inkeep Agent Framework provides two layers of access control:

| Layer              | What it does                                         |
| ------------------ | ---------------------------------------------------- |
| **Authentication** | Users sign in, belong to organizations, manage teams |
| **Authorization**  | Fine-grained project-level roles and permissions     |

Authentication handles user sign-in and organization membership. Authorization adds granular control over who can do what within each project.

<Note>
  For deployment configuration including OAuth providers, see [Authentication Setup](/deployment/authentication).
</Note>

## Sign-In Methods

| Method               | Description                                                    |
| -------------------- | -------------------------------------------------------------- |
| **Email & Password** | Default sign-in with email and password credentials            |
| **Google**           | OAuth sign-in (requires configuration)                         |
| **Microsoft**        | OAuth sign-in (requires configuration)                         |
| **SSO (OIDC)**       | Enterprise SSO with your identity provider (Inkeep Cloud only) |

To add Google or Microsoft sign-in for self-hosted deployments, see [Adding OAuth Providers](/deployment/authentication#adding-oauth-providers).

Organization admins can enable or disable sign-in methods from **Settings** → **Authentication Methods**. Toggle the switch next to each method to control which options are available to your team.

<Note>
  At least one authentication method must remain enabled at all times.
</Note>

For enterprise SSO with custom OIDC identity providers (Okta, Entra ID, Google Workspace, etc.), see [SSO Configuration](/visual-builder/sso-configuration).

## Organizations & Team Management

Each organization operates as an isolated tenant:

* **Separate workspaces**: Each organization has its own projects, agents, MCP servers, and credentials
* **Team collaboration**: Multiple users can belong to the same organization
* **Role-based access**: Team members have different permission levels

### Organization Roles

| Role       | Permissions                                               |
| ---------- | --------------------------------------------------------- |
| **Admin**  | Full access to all projects and settings, can add members |
| **Member** | Access determined by project-level roles                  |

### Inviting Team Members

1. Go to **Settings** in the left sidebar
2. View current members and their roles
3. Click **Invite** to invite a new team member
4. Enter the member's email and select a role (Admin or Member)
5. Click **Send Invitation**

If email is enabled for your tenant, the invited user receives a branded email with a link to accept the invitation and set up their account. The invitation expires after 7 days.

If email is not configured, the invitation link is displayed directly in the UI — copy and share it manually with the team member.

<Tip>
  For self-hosted deployments, email requires SMTP configuration. See [Configure Email](/deployment/email) for setup instructions.
</Tip>

## Password Reset

If email is enabled for your tenant, users can reset their password through a self-service flow:

1. On the sign-in page, click **Forgot password?**
2. Enter the email address associated with your account
3. Check your inbox for a password reset link
4. Click the link and set a new password

The reset link expires after 30 minutes. If you don't receive the email, check your spam folder.

If email is not configured for your tenant, contact your organization administrator to reset your password.

## Active Sessions

View every device currently signed in to your account and revoke any session you no longer recognize.

To open the surface:

1. Click your avatar in the top-right corner
2. Select **Profile**
3. Scroll to the **Active sessions** section below your profile details

Each row represents one signed-in device and shows:

| Field           | What it shows                                                                                                                |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **Device**      | Browser and operating system (e.g., "Safari on macOS"). Falls back to "Unknown device" when the user-agent cannot be parsed. |
| **IP address**  | The IP address recorded when the session was created. Displays "—" when unavailable.                                         |
| **Signed in**   | When the session was first established.                                                                                      |
| **Last active** | The most recent time the session made an authenticated request.                                                              |
| **Expires**     | When the session will be automatically terminated.                                                                           |
| **This device** | A badge marks the session you are currently using. It is always pinned to the top of the list.                               |

### Revoking a Single Session

1. Click **Revoke** on any session row
2. Confirm in the dialog
3. The row disappears from the list and a confirmation toast appears

Revoking your own current session signs you out of the current device and redirects you to the login page.

### Revoking All Other Sessions

When you suspect a device has been compromised, use the panic button to terminate every other session in one step.

1. Click **Revoke all other sessions** at the top of the section
2. The confirmation dialog displays how many sessions will be terminated
3. Confirm to revoke every session except your current one

The button is hidden when only your current session exists.

<Note>
  A revoked session may continue to work for up to 30 seconds on the revoked device. This is the maximum age of the server-side session cache and the lag cannot be reduced without affecting performance.
</Note>

<Tip>
  Every session deletion is recorded in the server logs with the associated user and session identifiers, so administrators can audit revocation events through log search.
</Tip>

## Project Roles & Permissions

Assign granular roles at the project level to give organization Members specific access to individual projects.

### Role Hierarchy

| Role               | View | Use | Edit |
| ------------------ | :--: | :-: | :--: |
| **Project Admin**  |   ✓  |  ✓  |   ✓  |
| **Project Member** |   ✓  |  ✓  |   ✗  |
| **Project Viewer** |   ✓  |  ✗  |   ✗  |

### Permission Breakdown

| Permission | What it allows                                                                                          |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| **View**   | See project configuration, agents, and settings (read-only)                                             |
| **Use**    | Invoke agents, create API keys, view traces                                                             |
| **Edit**   | Modify agents, tools, credentials, project settings, and evaluations (test suites, evaluators, configs) |

### Managing Project Members

1. Navigate to your project
2. Go to **Members**
3. Search for members by email and select one or more to add
4. Choose a role for the selected members and click **Add**

<Tip>
  Organization Admins always have full access to all projects, regardless of project-level roles.
</Tip>

## User-Scoped vs Project-Scoped Resources

Certain resources can be configured with different scopes:

| Scope              | Description                            |
| ------------------ | -------------------------------------- |
| **Project-scoped** | Shared across all users in the project |
| **User-scoped**    | Configured separately for each user    |

### Example: MCP Servers

MCP servers can be configured as either project-scoped or user-scoped:

| Use Case                                            | Recommended Scope |
| --------------------------------------------------- | ----------------- |
| Shared company tools (internal APIs, databases)     | Project-scoped    |
| Personal integrations (user's Slack, GitHub, email) | User-scoped       |
| Services requiring per-user authorization           | User-scoped       |
| Tools where data should be separated by user        | User-scoped       |

<Tip>
  You only configure a user-scoped MCP server once. Each user sees the same server but connects with their own credentials. The framework automatically manages the per-user authentication.
</Tip>

To select the scope, go to **MCP Servers** → create a server → select the scope.

See [MCP Servers](/visual-builder/tools/mcp-servers) for more details.


# Context Fetchers in Visual Builder (/visual-builder/context-fetchers)

Set up context fetchers in the Visual Builder to pull real-time data from external APIs into agent prompts.



## Overview

Context fetchers allow you to embed real-time data from external APIs into your agent prompts. Instead of hardcoding information in your agent prompt, context fetchers dynamically retrieve fresh data for each conversation.

## Key Features

* **Dynamic data retrieval**: Fetch real-time data from APIs.
* **Dynamic Prompting**: Use dynamic data in your agent prompts
* **Headers integration**: Use request-specific parameters to customize data fetching.
* **Data transformation**: Transform API responses into the exact format your agent needs.

## Context Fetchers vs Tools

* **Context Fetchers**: Pre-populate agent prompts with dynamic data
  * Run automatically before/during conversation startup
  * Data becomes part of the agent's system prompt
  * Perfect for: Personalized agent personas, dynamic agent guardrails
  * Example Prompt: `You are an assistant for {{user.name}} and you work for {{user.organization}}`

* **Tools**: Enable agents to take actions or fetch data during conversations
  * Called by the agent when needed during the conversation
  * Agent decides when and how to use them
  * Example Tool Usage: Agent calls a "send\_email" tool or "search\_database" tool

## Basic Usage

1. Go to the Agents tab in the left sidebar. Then click on the agent you want to configure.
2. On the right pane scroll down to the "Context Variables" section.
3. Add your context variables in JSON format.
4. Click on the "Save" button.

## Defining Context Variables

The keys that you define in the Context Variables JSON object are used to reference fetched data in your agent prompts.
Each key in the JSON should map to a fetch definition with the following properties:

* **`id`** (required): Unique identifier for the fetch definition
* **`name`** (optional): Human-readable name for the fetch definition
* **`trigger`** (required): When to execute the fetch:
  * `"initialization"`: Fetch only once when a conversation is started with the agent
  * `"invocation"`: Fetch every time a request is made to the agent
* **`fetchConfig`** (required): HTTP request configuration:
  * **`url`** (required): The API endpoint URL (supports template variables)
  * **`method`** (optional): HTTP method - `GET`, `POST`, `PUT`, `DELETE`, or `PATCH` (defaults to `GET`)
  * **`headers`** (optional): Object with string key-value pairs for HTTP headers
  * **`body`** (optional): Request body for POST/PUT/PATCH requests
  * **`transform`** (optional): JSONPath expression or JavaScript transform function to extract specific data from the response
  * **`timeout`** (optional): Request timeout in milliseconds (defaults to 10000)
  * **`requiredToFetch`** (optional): Array of template variables that must resolve to non-empty values for the fetch to execute. If any variable is missing or empty, the fetch is skipped and `defaultValue` is used instead. Useful for optional fetches that depend on request headers.
* **`responseSchema`** (optional): Valid JSON Schema object to validate the API response structure.
* **`defaultValue`** (optional): Default value to use if the fetch fails or returns no data
* **`credentialReferenceId`** (optional): The ID of a stored credential to use for authentication. The credential's secret value will be injected into the request headers at runtime. See [Finding the Credential Reference ID](#finding-the-credential-reference-id) below.

Here is an example of a valid Context Variables JSON object:

```json
{
  "timeInfo": {
    "id": "time-info",
    "name": "Time Information",
    "trigger": "invocation",
    "fetchConfig": {
      "url": "https://world-time-api3.p.rapidapi.com/timezone/US/Pacific",
      "method": "GET",
      "headers": {
        "x-rapidapi-key": "590c52974dmsh0da44377420ef4bp1c64ebjsnf8d55149e28d"
      }
    },
    "defaultValue": "Unable to fetch time information",
    "responseSchema": {
      "type": "object",
      "$schema": "http://json-schema.org/draft-07/schema#",
      "required": [
        "datetime"
      ],
      "properties": {
        "datetime": {
          "type": "string"
        },
        "timezone": {
          "type": "string"
        }
      }
    }
  }
}
```

## Using Credentials

If your API requires authentication via a stored credential, add the `credentialReferenceId` field to your context variable definition:

```json
{
  "projectDescription": {
    "id": "project-info",
    "name": "Project Information",
    "trigger": "initialization",
    "fetchConfig": {
      "url": "https://api.example.com/graphql",
      "body": {
        "query": "query GetProjectInfo { project { name description } }"
      },
      "method": "POST",
      "headers": {
        "Content-Type": "application/json"
      }
    },
    "defaultValue": "Unable to fetch project information",
    "credentialReferenceId": "u6zq98tz0lkloakuqbpdv"
  }
}
```

### Finding the Credential Reference ID

To find the credential reference ID:

1. Navigate to the **Credentials** page in the Inkeep dashboard.
2. Click on the credential you want to use.
3. Copy the ID from the browser URL — it is the last path segment. For example, in `https://app.inkeep.com/.../credentials/u6zq98tz0lkloakuqbpdv`, the credential reference ID is `u6zq98tz0lkloakuqbpdv`.

## Optional Context Fetches

Use `requiredToFetch` when you want a fetch to only execute when certain headers are provided. This is useful for optional data that enhances the agent experience but isn't required.

Here's an example that fetches conversation history only when the `x-conversation-id` header is provided:

```json
{
  "conversationHistory": {
    "id": "conversation-history",
    "name": "Conversation History",
    "trigger": "initialization",
    "fetchConfig": {
      "url": "https://api.example.com/conversations/{{headers.x-conversation-id}}",
      "method": "GET",
      "requiredToFetch": ["{{headers.x-conversation-id}}"]
    },
    "defaultValue": { "messages": [] }
  }
}
```

When `x-conversation-id` is not provided in the request headers:

* The fetch is **skipped** (not treated as an error)
* The `defaultValue` is used instead
* The agent continues normally with an empty message history

## Using Context Variables

Once you have defined your context variables, you can use them in your agent prompts.

1. Click on the agent you want to modify.
2. In the "Prompt" section, you can embed fetched data in the prompt using the key defined in the "Context Variables" section. Reference them using double curly braces `{{}}`.

Here is an example of an agent prompt using the context variable defined above:

```
You are a helpful assistant, the time in the US Pacific timezone is {{timeInfo.datetime}}.
```


# Evaluations in Visual Builder (/visual-builder/evaluations)

Configure evaluators, batch evaluations, and continuous tests in the Visual Builder



The Visual Builder lets you define, manage, and run evaluations. You define evaluators (how to score agents), then run them in two ways: **batch evaluations** (one-time jobs over selected conversations) and **continuous tests** (automatic evaluation on a sample of live conversations). For **fixed test cases** (curated prompts and optional gold outputs), use **[test suites](/visual-builder/test-suites)** and test suite runs instead of batch evaluation over conversation history.

## Where to find evaluations

1. Open your project in the Visual Builder.
2. In the project sidebar, go to **Evaluations** for evaluators, batch jobs, and continuous tests.

<Note>
  You need **Edit** permission on the project to create or change evaluators, batch evaluations, and continuous tests. See [Access control](/visual-builder/access-control) for roles and permissions.
</Note>

## Evaluators

Evaluators define how agent responses are scored. Each evaluator has a **prompt**, a **schema**, and optionally a **pass criteria** to produce a score or structured output.

### Creating an evaluator

1. Go to **Evaluations** and open the **Evaluators** tab.

2. Click **New evaluator**.

3. Fill in:
   * **Name** and optional **Description**
   * **Prompt** — instructions for the model (e.g. what to score and how)
   * **Schema** — JSON schema for structured output (e.g. numeric score, categories)
   * **Model** — the model used to run the evaluator
   * **Pass criteria** (optional) — conditions on numeric schema fields that define pass/fail (e.g. `score >= 0.8`)

4. Save. The evaluator is then available for batch evaluations and continuous tests.

### Example Evaluator

<Image src="/images/evaluator-example.png" alt="Evaluator form in the Visual Builder showing name, prompt, schema, model, and pass criteria fields" />

### Editing or deleting

From the Evaluators list, open an evaluator to view or edit it, or use the delete action.

## Batch evaluations

Batch evaluations run selected evaluators over a set of conversations once. In the Visual Builder you scope that set with a **date range**. The API and TypeScript SDK can also scope a job to specific **test suite runs** using test suite run IDs (`datasetRunIds` in the API)—for example to evaluate only conversations produced by a given [test suite](/visual-builder/test-suites) run. Starting a test suite run **with evaluators** creates a matching batch evaluation automatically.

### Creating a batch evaluation

1. Go to **Evaluations** and open the **Batch Evaluations** tab.
2. Click **New batch evaluation**.
3. Select one or more **Evaluators**.
4. Narrow the scope by **Date range** — only conversations within that range.
5. Start the job. A new batch evaluation job is created and runs asynchronously.

For run-scoped jobs, use the [Evaluations API](/api-reference/evaluations) or [`triggerBatchEvaluation`](/typescript-sdk/evaluations) with `datasetRunIds`, or attach evaluators when you trigger a test suite run from **Test Suites**.

### Viewing results

From the Batch Evaluations list, open a job to see its **results**: per-conversation evaluation outputs, pass/fail if pass criteria are set, and status. You can filter and inspect individual results.

## Continuous tests

Continuous tests evaluate a sample of **live** conversations automatically. You specify which evaluators to run, which agents (optional), and a **sample rate** (e.g. 10% of conversations).

### Creating a continuous test

1. Go to **Evaluations** and open the **Continuous Tests** tab.
2. Click **New continuous test**.
3. Set **Name** and optional **Description**.
4. The config is **Active** so it runs on new conversations.
5. Choose **Evaluators** to run.
6. Optionally restrict by **Agents** (only evaluate conversations for those agents).
7. Set **Sample rate** (0–1) to evaluate a fraction of matching conversations.
8. Save. Once active, matching conversations will be evaluated according to the sample rate.

### Viewing results

From the Continuous Tests list, open a config to see **Results** for that run config: all evaluation results triggered by that continuous test, with filters.

## Summary

| Area                  | Purpose                                                                                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Evaluators**        | Define how to score agent outputs (prompt, model, schema, pass criteria).                                                                                    |
| **Batch evaluations** | Run evaluators once over a scoped set of conversations (date range).                                                                                         |
| **Continuous tests**  | Automatically run evaluators on a sample of live conversations.                                                                                              |
| **Test suites**       | Curated items (input messages and optional expected output); run agents against them via run configurations. See [Test suites](/visual-builder/test-suites). |

For programmatic access to the same concepts, see [TypeScript SDK: Evaluations](/typescript-sdk/evaluations) and the [Evaluations API reference](/api-reference/evaluations).


# Execution Modes (/visual-builder/execution-modes)

Choose between classic and durable execution modes for your agents.



Every agent runs in one of two execution modes: **Classic** or **Durable**. The mode determines how the agent's execution lifecycle is managed — specifically how state is handled during LLM calls, tool execution, and error recovery.

## Classic mode (default)

Classic mode runs the entire agent request in a single in-process loop. The LLM call, tool execution, and response streaming all happen within one HTTP request lifecycle.

* Tokens stream to the client word-by-word with minimal latency (\~100ms time-to-first-byte)
* Tool approvals are supported
* Stream resumption is supported via persisted stream chunks
* If the server process crashes mid-execution, the request is lost

**Best for:** Interactive chat where streaming responsiveness matters most.

## Durable mode

Durable mode wraps agent execution in a workflow with persistent checkpoints. Each phase of execution (initialize, call LLM, execute tools, complete) is a separate step that persists its result before proceeding.

* Execution state is persisted to the workflow backend
* If the server crashes, the workflow can resume from the last completed step
* Tool approval suspends the workflow until the user responds, then resumes
* Stream data is persisted by the workflow runtime for client reconnection
* Higher time-to-first-byte (\~2s) due to workflow step transitions

**Best for:** Agents with tool approval workflows, long-running executions, or environments where crash recovery is important.

## Choosing a mode

| Consideration                 | Classic                    | Durable                      |
| ----------------------------- | -------------------------- | ---------------------------- |
| **Streaming latency**         | \~100ms TTFB               | \~2s TTFB                    |
| **Crash recovery**            | No                         | Yes                          |
| **Tool approval**             | In-memory (request-scoped) | Persistent (workflow-scoped) |
| **Multi-step tool execution** | Same request               | Separate workflow steps      |
| **Stream resumption**         | Via stream chunks table    | Via workflow storage         |

For most interactive chat use cases, **Classic** mode provides the best user experience. Switch to **Durable** when you need persistent tool approvals or crash recovery guarantees.

## Setting the execution mode

### In the Visual Builder

Open your agent's settings panel and find the **Execution mode** section. Select either **Classic** or **Durable** from the dropdown. This sets the default for all requests to this agent.

### Per-request override

Clients can override the agent's default execution mode by passing `executionMode` in the request body:

```json
{
  "messages": [{ "role": "user", "content": "Hello" }],
  "executionMode": "durable"
}
```

When provided, `executionMode` in the request takes precedence over the agent's configured default. This allows the same agent to handle both low-latency interactive chat (classic) and crash-recoverable workflows (durable) depending on the client's needs.


# Headers in Visual Builder (/visual-builder/headers)

Configure HTTP headers in the Visual Builder to pass dynamic context for personalized agent interactions.



## Overview

Headers allow you to pass request-specific values (like user IDs, authentication tokens, or organization metadata) to your agent at runtime via HTTP headers. These values are validated and made available throughout your agent system for:

* **Context Fetchers**: Dynamic data retrieval based on request values
* **External Tools**: Authentication and personalization for API calls
* **Agent Prompts**: Personalized responses using context variables

## Configuring Headers

1. Go to the Agents tab in the left sidebar. Then click on the agent you want to configure.
2. On the right pane scroll down to the "Headers schema" section.
3. Enter the schema in JSON Schema format.
4. Click on the "Save" button.

Here is an example of a valid headers schema:

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "user-id": {
      "type": "string"
    }
  },
  "required": [
    "user-id"
  ],
  "additionalProperties": {}
}
```

<Warning>
  Header keys are converted to **lowercase** before validation — for example, `Auth-Token` becomes `auth-token`. No other transformation is applied. A mismatch will cause a `Context validation failed` error.
</Warning>

You can generate custom schemas using this [JSON Schema generator](https://transform.tools/json-to-json-schema).

## Sending Custom Headers

1. On your agent page, click on the **Try it** button in the top right corner.
2. Click on the "Custom headers" button in the top right corner.
3. Enter the custom headers in JSON format.
4. Click on the "Apply" button.

## Header Validation

When you enter custom headers in the playground, they are validated in real-time against the configured headers schema. If the headers are invalid or missing required fields, the chat input is disabled and an error message is displayed. You must fix validation errors before you can send messages.

## Using Headers in Your Agent Prompts

1. Go to the Agents tab in the left sidebar. Then click on the agent you want to configure.
2. Either add a new agent or edit an existing agent by clicking on the agent you want to edit.
3. On the right pane scroll down to the "Prompt" section.
4. Use the double curly braces `{{}}` to reference the headers variables.

Here is an example of a valid prompt:

```text
You are a helpful assistant for {{headers.user-id}}!.
```

## Forwarding Headers to MCP Servers

You can forward header values to MCP servers using `{{headers.*}}` templates. These templates are resolved at runtime from the incoming request headers before each tool call.

1. Define the header in your agent's [headers schema](#configuring-headers) (use lowercase keys)
2. [Register the MCP server](/visual-builder/tools/mcp-servers) — leave the Credential field blank if auth comes entirely from forwarded headers
3. Connect the MCP server to a Sub Agent, then click on the connection. In the **Headers** field, reference the schema properties:

```json
{
  "Authorization": "Bearer {{headers.auth-token}}",
  "Tenant-Id": "{{headers.tenant-id}}"
}
```

See [MCP Servers — Dynamic credential pass-through](/visual-builder/tools/mcp-servers#dynamic-credential-pass-through) for more on authentication.


# Outbound Webhooks (/visual-builder/outbound-webhooks)

Receive real-time HTTP notifications when conversations, feedback, or custom events occur in your project



Outbound webhooks send HTTP POST requests to your server when events happen in your project — new conversations, conversation updates, feedback submissions, and custom events. Use them to sync data with external systems, trigger downstream workflows, or build real-time integrations.

<Note>
  Outbound webhooks push data *from* Inkeep *to* your server. For the reverse — external services triggering your agents — see [Webhook Triggers](/visual-builder/triggers/webhooks).
</Note>

## Create a webhook

<Steps>
  <Step>
    Navigate to your project and select **Outbound Webhooks** in the left sidebar.

    Click **New Webhook**.
  </Step>

  <Step>
    ### Configure the destination

    Fill in the required fields:

    <Image src="/images/outbound-webhooks.png" alt="Webhook destination form with name, URL, custom headers, event type checkboxes, and agent scope" />

    | Field               | Required | Description                                                                            |
    | ------------------- | -------- | -------------------------------------------------------------------------------------- |
    | **Name**            | Yes      | Human-readable identifier (e.g., "Slack Notifications" or "CRM Sync")                  |
    | **Destination URL** | Yes      | The HTTPS endpoint that receives webhook POST requests                                 |
    | **Event Types**     | Yes      | Which events trigger delivery (select at least one)                                    |
    | **Description**     | No       | Explains what this webhook does                                                        |
    | **Custom Headers**  | No       | Additional HTTP headers sent with every delivery                                       |
    | **Agents**          | No       | Scope to specific agents. Leave empty to receive events from all agents in the project |
    | **Enabled**         | —        | Toggle on to activate immediately                                                      |
  </Step>

  <Step>
    ### Test the destination

    After creating the destination, use the **Test** action to send a sample payload to your URL. The test request uses a `test` event type with synthetic data and a 10-second timeout, so you can verify your endpoint is reachable before real events fire.
  </Step>
</Steps>

## Event types

Each webhook destination subscribes to one or more event types. Your endpoint receives every event that matches the selected types and agent scope.

| Event type                      | Fires when                                                                                     |
| ------------------------------- | ---------------------------------------------------------------------------------------------- |
| `conversation.created`          | A new conversation starts                                                                      |
| `conversation.updated`          | A user sends a message or the assistant replies                                                |
| `feedback.created`              | A user submits feedback on a conversation                                                      |
| `event.created`                 | A custom event is logged via `POST /run/v1/events`                                             |
| `evaluation.failed`             | An evaluation result does not meet its pass criteria                                           |
| `conversation.execution.error`  | Conversation reaches terminal failure                                                          |
| `conversation.generation.error` | An LLM call fails to produce a response                                                        |
| `conversation.tool.error`       | A tool call fails. For MCP tools, only fires when all calls to the server fail in a generation |
| `conversation.context.error`    | Context resolution encounters fetch failures                                                   |

### Agent scoping

When you select specific agents on a webhook destination, only events originating from those agents trigger delivery. Leave the agents field empty to receive events from every agent in the project.

### Evaluator scoping

For `evaluation.failed` events, you can further scope a webhook destination to specific evaluators. When evaluators are selected, only failures from those evaluators trigger delivery. Leave the evaluators field empty to receive `evaluation.failed` events from all evaluators in the project.

## Payload format

Every delivery is an HTTP POST with `Content-Type: application/json`. The body follows a consistent envelope:

```json
{
  "type": "conversation.updated",
  "timestamp": "2026-05-12T14:30:00.000Z",
  "tenantId": "your-tenant-id",
  "projectId": "your-project-id",
  "agentId": "your-agent-id",
  "agentName": "Support Agent",
  "data": {
    // event-specific payload (see below)
  }
}
```

### Conversation events

`conversation.created` and `conversation.updated` both deliver a `conversation` object inside `data`:

```json
{
  "type": "conversation.created",
  "timestamp": "2026-05-12T14:30:00.000Z",
  "tenantId": "your-tenant-id",
  "projectId": "your-project-id",
  "agentId": "your-agent-id",
  "agentName": "Support Agent",
  "data": {
    "conversation": {
      "id": "conv_abc123",
      "agentId": "your-agent-id",
      "title": "How do I reset my password?",
      "userProperties": { "userId": "u_456" },
      "properties": { "pageUrl": "https://example.com/help" },
      "createdAt": "2026-05-12T14:29:50.000Z",
      "updatedAt": "2026-05-12T14:30:00.000Z",
      "messages": [
        {
          "id": "msg_001",
          "role": "user",
          "content": "How do I reset my password?",
          "parts": [{ "type": "text", "text": "How do I reset my password?" }],
          "createdAt": "2026-05-12T14:29:50.000Z"
        },
        {
          "id": "msg_002",
          "role": "assistant",
          "content": "You can reset your password from the Settings page...",
          "parts": [
            { "type": "text", "text": "You can reset your password from the Settings page..." }
          ],
          "createdAt": "2026-05-12T14:30:00.000Z"
        }
      ]
    }
  }
}
```

### Feedback events

`feedback.created` delivers both the feedback and the associated conversation:

```json
{
  "type": "feedback.created",
  "timestamp": "2026-05-12T14:31:00.000Z",
  "tenantId": "your-tenant-id",
  "projectId": "your-project-id",
  "agentId": "your-agent-id",
  "agentName": "Support Agent",
  "data": {
    "feedback": {
      "id": "fb_789",
      "conversationId": "conv_abc123",
      "messageId": "msg_002",
      "type": "positive",
      "details": null,
      "createdAt": "2026-05-12T14:31:00.000Z",
      "updatedAt": "2026-05-12T14:31:00.000Z"
    },
    "conversation": {
      "id": "conv_abc123",
      "agentId": "your-agent-id",
      "title": "How do I reset my password?",
      "userProperties": null,
      "properties": null,
      "createdAt": "2026-05-12T14:29:50.000Z",
      "updatedAt": "2026-05-12T14:30:00.000Z",
      "messages": [
        {
          "id": "msg_001",
          "role": "user",
          "content": "How do I reset my password?",
          "parts": [{ "type": "text", "text": "How do I reset my password?" }],
          "createdAt": "2026-05-12T14:29:50.000Z"
        },
        {
          "id": "msg_002",
          "role": "assistant",
          "content": "You can reset your password from the Settings page...",
          "parts": [
            { "type": "text", "text": "You can reset your password from the Settings page..." }
          ],
          "createdAt": "2026-05-12T14:30:00.000Z"
        }
      ]
    }
  }
}
```

### Evaluation events

`evaluation.failed` fires when an evaluation result does not meet its pass criteria. The payload includes the evaluator name, the conversation ID, and the specific conditions that failed:

```json
{
  "type": "evaluation.failed",
  "timestamp": "2026-05-12T14:33:00.000Z",
  "tenantId": "your-tenant-id",
  "projectId": "your-project-id",
  "agentId": "your-agent-id",
  "agentName": "Support Agent",
  "data": {
    "evaluator": { "id": "evaluator_abc", "name": "Response Quality" },
    "conversation": {
      "id": "conv_abc123",
      "userProperties": { "id": "u_456", "name": "Jane", "email": "jane@example.com" },
      "properties": { "pageUrl": "https://example.com/help" }
    },
    "failedConditions": [
      {
        "field": "score",
        "operator": ">=",
        "value": 0.7,
        "actual": 0.3
      }
    ]
  }
}
```

### Custom events

`event.created` delivers the persisted event object:

```json
{
  "type": "event.created",
  "timestamp": "2026-05-12T14:32:00.000Z",
  "tenantId": "your-tenant-id",
  "projectId": "your-project-id",
  "agentId": "your-agent-id",
  "agentName": "Support Agent",
  "data": {
    "event": {
      "id": "evt_xyz",
      "type": "user_message_submitted",
      "agentId": "your-agent-id",
      "conversationId": "conv_abc123",
      "messageId": "msg_001",
      "properties": { "source": "chat-widget" },
      "userProperties": { "userId": "u_456" },
      "metadata": { "appVersion": "1.4.2" },
      "serverMetadata": { "authMethod": "apiKey" },
      "createdAt": "2026-05-12T14:32:00.000Z",
      "updatedAt": "2026-05-12T14:32:00.000Z"
    }
  }
}
```

## Slack integration

Webhook destinations pointing to Slack automatically receive rich formatted messages instead of raw JSON. This applies whether the URL was added via the OAuth flow or typed manually.

### Connect a Slack channel

<Steps>
  <Step>
    On the **Outbound Webhooks** page, click **Connect Slack Channel**. This starts a Slack OAuth flow that lets you pick a channel and grants an incoming-webhook URL.
  </Step>

  <Step>
    After authorizing, you're redirected back to the webhook creation form with the Slack URL pre-filled. Select your event types and click **Create**.
  </Step>
</Steps>

### Slack payload format

```json
{
  "type": "conversation.created",
  "timestamp": "2026-05-12T14:30:00.000Z",
  "tenantId": "your-tenant-id",
  "projectId": "your-project-id",
  "agentId": "your-agent-id",
  "agentName": "Support Agent",
  "data": {
    "conversation": { "id": "conv_abc123", "title": "..." }
  },
  "text": "New Conversation: conv id: conv_abc123",
  "blocks": [
    { "type": "header", "text": { "type": "plain_text", "text": "New Conversation" } },
    { "type": "section", "fields": ["..."] },
    { "type": "section", "text": { "type": "mrkdwn", "text": "<link|View Conversation>" } },
    { "type": "context", "elements": [{ "type": "mrkdwn", "text": "Inkeep" }] }
  ]
}
```

The `text` field serves as the notification preview (mobile push, channel sidebar). The `blocks` array renders the rich card in the channel. Non-Slack URLs always receive the standard JSON envelope without `text` or `blocks`.

## Delivery and retries

Inkeep delivers each event to each matching destination independently with automatic retries.

| Behavior          | Detail                 |
| ----------------- | ---------------------- |
| **Method**        | `POST`                 |
| **Timeout**       | 30 seconds per attempt |
| **Max attempts**  | 3                      |
| **Retry backoff** | Exponential: 2s, 4s    |

## Custom headers

Webhook deliveries include any **custom headers** you configure on the destination.

<Warning>
  The following reserved headers cannot be used as custom headers: `Connection`, `Keep-Alive`, `TE`, `Trailer`, `Transfer-Encoding`, `Upgrade`, `Proxy-Authorization`, `Proxy-Connection`, `Content-Length`.
</Warning>

## Manage webhook destinations

The webhook destinations page shows all configured destinations with their name, URL, enabled status, and subscribed event types.

| Action     | Description                                                           |
| ---------- | --------------------------------------------------------------------- |
| **Edit**   | Update the name, URL, events, headers, agent scope, or enabled status |
| **Test**   | Send a sample payload to verify the destination is reachable          |
| **Delete** | Permanently remove the destination                                    |

Toggle the **Enabled** switch to pause or resume delivery without deleting the destination.

## Conversation error events

Subscribe to error events to get notified when agent conversations encounter failures. Four error event types are available:

| Event type                      | Fires when                                                                                                     |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `conversation.execution.error`  | Conversation reaches terminal failure (max consecutive errors, max transfers exhausted, or uncaught exception) |
| `conversation.generation.error` | An LLM call fails to produce a response                                                                        |
| `conversation.tool.error`       | A tool call fails. For MCP tools, only fires when all calls to the server fail in a generation                 |
| `conversation.context.error`    | Context resolution encounters fetch failures                                                                   |

### Subscribing

In the webhook destination form, use the **Conversation error** checkbox to subscribe to all four error types at once. Click **Advanced** to select specific error types individually.

Each error event type is independently subscribable via the `eventTypes` array — subscribe only to the types you care about.

### Example payloads

**conversation.execution.error**

```json
{
  "type": "conversation.execution.error",
  "timestamp": "2026-05-26T19:30:00.000Z",
  "tenantId": "tenant_123",
  "projectId": "proj_456",
  "agentId": "agent_789",
  "agentName": "Support Agent",
  "data": {
    "conversation": {
      "id": "conv_abc",
      "userProperties": { "id": "u_456", "name": "Jane", "email": "jane@example.com" },
      "properties": { "pageUrl": "https://example.com/help" }
    },
    "reason": "Maximum error limit (3) reached"
  }
}
```

**conversation.generation.error**

```json
{
  "type": "conversation.generation.error",
  "timestamp": "2026-05-26T19:30:00.000Z",
  "tenantId": "tenant_123",
  "projectId": "proj_456",
  "agentId": "agent_789",
  "agentName": "Support Agent",
  "data": {
    "conversation": {
      "id": "conv_abc",
      "userProperties": { "id": "u_456", "name": "Jane", "email": "jane@example.com" },
      "properties": { "pageUrl": "https://example.com/help" }
    },
    "reason": "Generation terminated by timeout/abort signal"
  }
}
```

**conversation.tool.error**

For MCP tools, this event only fires when **all** calls to the same server fail during a generation. If at least one call to the server succeeds, the failures are treated as warnings and no webhook is sent.

```json
{
  "type": "conversation.tool.error",
  "timestamp": "2026-05-26T19:30:00.000Z",
  "tenantId": "tenant_123",
  "projectId": "proj_456",
  "agentId": "agent_789",
  "agentName": "Support Agent",
  "data": {
    "conversation": {
      "id": "conv_abc",
      "userProperties": { "id": "u_456", "name": "Jane", "email": "jane@example.com" },
      "properties": { "pageUrl": "https://example.com/help" }
    },
    "tool": { "id": "tool_123", "name": "search_docs" },
    "mcpServer": { "id": "mcp_456", "name": "docs-server" },
    "reason": "Connection timeout after 30s"
  }
}
```

**conversation.context.error**

```json
{
  "type": "conversation.context.error",
  "timestamp": "2026-05-26T19:30:00.000Z",
  "tenantId": "tenant_123",
  "projectId": "proj_456",
  "agentId": "agent_789",
  "agentName": "Support Agent",
  "data": {
    "conversation": {
      "id": "conv_abc",
      "userProperties": { "id": "u_456", "name": "Jane", "email": "jane@example.com" },
      "properties": { "pageUrl": "https://example.com/help" }
    },
    "contextDefinition": { "id": "ctx_def_123" },
    "reason": "HTTP 500 from upstream"
  }
}
```

## Next steps

<Cards>
  <Card title="Events API" icon="LuActivity" href="/talk-to-your-agents/events">
    Log custom events that trigger `event.created` webhooks.
  </Card>

  <Card title="Webhook Triggers" icon="LuWebhook" href="/visual-builder/triggers/webhooks">
    Receive inbound webhooks from external services to trigger agent conversations.
  </Card>

  <Card title="API Reference" icon="LuBraces" href="/api-reference/webhooks">
    Manage webhook destinations via the REST API.
  </Card>
</Cards>


# Skills in Visual Builder (/visual-builder/skills)

Reusable instruction blocks you can attach to sub-agents



## Overview

Skills are reusable instruction blocks that you can attach to multiple sub-agents. They govern agent behavior,
reasoning, and tool usage, helping you keep shared rules, tone, and formatting consistent across the agent graph
while still allowing each sub-agent to focus on its own role.

When a skill is synced from code with `inkeep push`, Inkeep stores the full skill directory. `SKILL.md` remains the entry file, and any nested reference files or templates under the skill directory are synced with it.

## Browse and edit synced files

The Skills view shows the full file tree for every synced skill directory. Opening a skill takes you to its `SKILL.md` entry file, and opening a nested reference file shows that file in a read-only editor by default.

Click **Edit** to open the full-page file editor.

* Editing `SKILL.md` updates the skill's description, metadata, and body from the file contents.
* Removing `SKILL.md` deletes the entire skill, because every skill must keep exactly one entry file.
* Removing any other file only removes that file from the synced skill directory.

### Create nested folders with file paths

When you add a file, use `/` in the file name to place it in a nested folder.

* `templates/checklist` with the `.md` extension creates `templates/checklist.md`.
* Folders appear automatically when you save a file at a nested path.
* Empty folders are not supported.

## Create a skill

1. Go to **Skills** in the left sidebar.
2. Click **Create skill**.
3. Fill in the required fields and save.

<figure className="[&>img]:border [&>img]:h-120 [&>img]:mx-auto [&>img]:w-auto">
  <Image src="/images/create-skill.png" alt="Create a new skill" />

  <figcaption className="mt-2 text-center text-sm">
    Create a new skill
  </figcaption>
</figure>

## Fields

* [**Name**](https://agentskills.io/specification#name-field): A short identifier.
* [**Description**](https://agentskills.io/specification#description-field): Explains when to use this skill. Write descriptions that help the agent understand when to load the skill.
* [**Content**](https://agentskills.io/specification#body-content): The Markdown body, write whatever helps agents perform the task effectively.
* [**Metadata (JSON)**](https://agentskills.io/specification#metadata-field): Additional properties not defined by [the Agent Skills spec](https://agentskills.io/home).

<Tip>
  Write descriptions that include "Use when..." or "Use if..." phrases. This helps the agent decide when to load on-demand skills.
</Tip>

## Writing effective descriptions

The description field is critical for on-demand skills. The agent uses it to decide whether to load the skill, so be specific about the scenarios where the skill applies.

**Good example:**

```
Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents or when the user mentions PDFs, forms, or document extraction.
```

**Poor example:**

```
Helps with PDFs.
```

Keep descriptions concise. They're capped at **1024 characters**, but shorter is better: an always-loaded skill's description — and the outline entry for every on-demand skill — sits in the system prompt on every turn, so a tight, trigger-first description both routes better and costs less context. See [Tuning Agents](/guides/agent-engineering/tuning-agents#keep-tool-and-skill-descriptions-tight) for the cost angle.

## Attach skills to sub-agents

Open an agent in the Visual Builder, select a sub-agent node, and use the **Skills** picker to attach one or more
skills.

<Callout type="warning">
  You can drag to reorder them to control 

  **their priority**

   (later entries weigh more).
</Callout>

<figure className="[&>img]:border">
  <Image src="/images/skills-sidepane.png" alt="Skills configuration in sidepane" />

  <figcaption className="mt-2 text-center text-sm">
    Skills configuration in sidepane
  </figcaption>
</figure>

## Always loaded and On-demand skills

Use **Always loaded** to include a skill in every prompt. It's off by default so skills load on demand unless you
enable it.

### How on-demand skills work

On-demand skills appear as an outline in the system prompt. When the agent determines a skill is relevant based on its description, it calls the built-in `load_skill` tool to retrieve the full content and any synced nested files. This keeps prompts compact while still giving agents access to specialized instructions and templates when needed.


# SSO Configuration (/visual-builder/sso-configuration)

Configure enterprise SSO providers for your organization on Inkeep Cloud



Organization admins can configure enterprise single sign-on (SSO) and control which authentication methods are available to their team — all from the **Settings** page.

<Note>
  SSO configuration is available on **Inkeep Cloud** deployments only. Self-hosted deployments configure OAuth providers at the deployment level. See [Authentication Setup](/deployment/authentication).
</Note>

## Prerequisites

* You must be an **Organization Admin** to access authentication settings.
* Navigate to **Settings** → **Authentication Methods** to manage sign-in options.

## Managing Authentication Methods

The **Authentication Methods** card lists every sign-in option available to your organization. Use the toggle on each row to enable or disable a method.

| Method                 | Description                                                              |
| ---------------------- | ------------------------------------------------------------------------ |
| **Email and password** | Members sign in with their email address and password                    |
| **Google**             | Members sign in with their Google account (available when configured)    |
| **Microsoft**          | Members sign in with their Microsoft account (available when configured) |
| **SSO providers**      | Custom OIDC providers you register (e.g., Okta, Entra ID)                |

<Warning>
  At least one sign-in method must remain enabled at all times. Disabling a method when it is the last active one is blocked.
</Warning>

## Adding an SSO Provider

1. Go to **Settings** → **Authentication Methods**
2. Click **Add SSO Provider** at the bottom of the list
3. Fill in the form fields described below
4. Copy the **Callback / Redirect URI** and add it to your identity provider (IdP) — see [Configuring Your Identity Provider](#configuring-your-identity-provider)
5. Click **Configure SSO**

### Form Fields

| Field                      | Required | Description                                                                                                           |
| -------------------------- | :------: | --------------------------------------------------------------------------------------------------------------------- |
| **Provider Name**          |     ✓    | Display name shown to users on the login page (e.g., `Okta`)                                                          |
| **Email Domain**           |     ✓    | Users whose email matches this domain are automatically routed to this IdP (e.g., `acmecorp.com`)                     |
| **Issuer URL**             |     ✓    | Your IdP's OIDC discovery base URL (e.g., `https://your-org.okta.com`). OIDC metadata is auto-detected from this URL. |
| **Client ID**              |     ✓    | The OAuth client ID from your IdP application                                                                         |
| **Client Secret**          |     ✓    | The OAuth client secret from your IdP application                                                                     |
| **Scopes**                 |          | Comma-separated OIDC scopes. Defaults to `openid, email, profile, offline_access`                                     |
| **Auto-provision members** |          | Toggle — see [Auto-Provisioning](#auto-provisioning-vs-invitation-based-access)                                       |

<Tip>
  If you use **Google Workspace** as your IdP, remove `offline_access` from the scopes list — Google Workspace does not support refresh tokens via the `offline_access` scope.
</Tip>

## Configuring Your Identity Provider

When you open the **Add SSO Provider** form, a read-only **Callback / Redirect URI** is displayed. Copy this URL and register it as the allowed redirect URI in your identity provider before saving.

The callback URL follows this pattern:

```
https://<your-api-domain>/api/auth/sso/callback/<provider-id>
```

The provider ID is generated automatically based on your organization slug. You can copy the full URL directly from the form.

## Auto-Provisioning vs Invitation-Based Access

The **Auto-provision members** toggle controls what happens when a user signs in via SSO for the first time.

| Setting               | Behavior                                                                             |
| --------------------- | ------------------------------------------------------------------------------------ |
| **Enabled** (default) | The user is automatically added as an organization member on their first SSO sign-in |
| **Disabled**          | The user must receive an invitation before they can access the organization          |

<Tip>
  Use **invitation-based access** when you want explicit control over who joins your organization, or when you need to assign specific roles (such as Admin) to certain users before they sign in.
</Tip>

<Note>
  If a pending invitation exists for a user, auto-provisioning is skipped and the invitation's role assignment is preserved instead.
</Note>

## Editing an SSO Provider

1. Go to **Settings** → **Authentication Methods**
2. Find the SSO provider row and click the **⋮** actions menu
3. Select **Edit**
4. Update any fields — leave **Client ID** or **Client Secret** blank to keep the existing values
5. Click **Save Changes**

You can update the display name, email domain, issuer URL, credentials, scopes, and auto-provision setting at any time.

## Removing an SSO Provider

1. Go to **Settings** → **Authentication Methods**
2. Find the SSO provider row and click the **⋮** actions menu
3. Select **Remove**
4. Confirm the removal in the dialog

<Warning>
  Removing a provider deletes its configuration permanently. Members who relied solely on this provider to sign in will need to use another authentication method. Ensure at least one other method is enabled before removing.
</Warning>


# Get started with the Visual Agent Builder (/visual-builder/sub-agents)

Create Agents with a No-Code Visual Agent Builder



## Overview

An Agent is the top-level entity you can chat and interact with in the Visual Builder.

An Agent is made up of one or more Sub Agents. The Sub Agents that make up an Agent can delegate or transfer control to each other, share context, or use tools to respond to a user or complete a task.

You can use the Visual Builder to add Sub Agents to an Agent, give Sub Agents tools, and connect Sub Agents with each other to establish their relationships.

## Creating your first Agent

<Video src="/videos/create-an-agent.mp4" />

1. Go to the Agents tab in the left sidebar. Then select "Create Agent".

2. Provide a name, prompt, and description.

### Agent Prompt

You can configure an Agent-level prompt that gets automatically added to all Sub Agents that make up the Agent. This provides consistent context and behavior guidelines so Sub Agents respond and act as one cohesive unit.

**Example uses:**

* Company-wide policies and tone guidelines
* Domain-specific context that applies to all sub-agents
* Consistent behavior rules across the sub-agent network

Once an Agent is created, you can always edit the prompt by:

1. Clicking on the **Agent Settings** button next to **Try it**
2. Modify your prompt in the "Agent Prompt" field
3. Click **Save**

## Default Sub Agent

When you create an Agent, it'll have a **Default Sub Agent** by default. This is the Sub Agent that first receives and processes a user message.

Configure its name and prompt, then click **Save**.

## Trying the Agent

To try the Agent, ensure it has at least one Sub Agent, then save the Agent by clicking the "Save changes" button in the top right corner.

Then, click the **Try it** button in the top right corner and type into the chat input.

<Video src="/videos/try-an-agent.mp4" />

## Adding tools

Tools are used to perform actions in the Agent. You can register MCP tools in the Visual Builder. See the [MCP Servers](/visual-builder/tools/mcp-servers) page to get started.

Once you have an MCP Server registered, you can add it to a Sub Agent by dragging and dropping the **MCP** block from the top left onto the Canvas.

To connect the tool to the Sub Agent, click the "+" icon at the top of the MCP block, then drag the connector to the Sub Agent. A line will appear, indicating that the tool is connected to the Sub Agent.

<Video src="/videos/add-tool-vb.mp4" />

<Note>
  To delete a tool, click on the tool box and then hit the backspace key.
</Note>

## Add other Sub Agents

When you have tasks that get more complex, you'll likely want to create more Sub Agents that are specialized in narrow tasks and have prompts and tools focused on their roles.

To create more Sub Agents:

1. Drag and drop a **Sub Agent** block from the top left toolbar onto the canvas.
2. Configure the Sub Agent with its own prompt and settings
3. Connect it to the parent Sub Agent by clicking the connector at the top of the Sub Agent box and dragging it to the main Sub Agent

When connected, a line will appear showing the relationship between the Sub Agents.

A few notes:

* To delete a Sub Agent, click on the Sub Agent box and then hit the backspace key.
* An Agent must have at least one Sub Agent. If you delete the default Sub Agent, add a new one before trying the Agent.

<Video src="/videos/add-sub-agent-vb.mp4" />

### Sub Agent configuration

The following are configurable options for the Sub Agent through the Visual Builder:

| Parameter             | Type   | Required | Description                                                                                                                                   |
| --------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                | string | Yes      | Human-readable name for the Sub Agent                                                                                                         |
| `description`         | string | No       | Brief description of the Sub Agent's purpose and capabilities                                                                                 |
| `prompt`              | string | Yes      | Detailed behavior guidelines and system prompt for the Sub Agent                                                                              |
| `model`               | string | No       | AI model identifier (e.g., "anthropic/claude-sonnet-4-20250514"). See [Model Configuration](/typescript-sdk/models)                           |
| `providerOptions`     | object | No       | Model-specific configuration options (temperature, maxOutputTokens, etc.)                                                                     |
| `fallbackModels`      | array  | No       | Ordered backup models if the primary fails. Requires [AI Gateway](/typescript-sdk/models#ai-gateway)                                          |
| `allowedProviders`    | array  | No       | Restrict and prioritize which providers serve requests. Requires [AI Gateway](/typescript-sdk/models#ai-gateway)                              |
| `Data components`     | object | No       | Data components that the Sub Agent can use. See [Data Components](/visual-builder/structured-outputs/data-components) for details             |
| `Artifact components` | array  | No       | Artifact components that the Sub Agent can use. See [Artifact Components](/visual-builder/structured-outputs/artifact-components) for details |

## Adding data components

Data components are used to render rich UI components directly in the chat. Before adding a data component, you will first need to create a data component. See the [Data Components](/visual-builder/structured-outputs/data-components) page for more information.

Then, add the data component to your Sub Agent by clicking the Sub Agent box and then selecting which data components you want to add.

## Adding artifact components

To create artifact components, see the [Artifact Components](/visual-builder/structured-outputs/artifact-components) page for details.

Add the artifact component to your Sub Agent by clicking on the Sub Agent box in the Agent, then selecting which artifact components you want to add.

When a Sub Agent uses a tool, it can choose to save the result as an artifact according to the defined schema. **Preview fields** (marked `inPreview: true` in the schema) are available immediately in the agent's context and streamed to clients. Non-preview fields are persisted in storage and can be retrieved on demand. Agents can also pass artifacts directly to tools, which receive the complete data including all non-preview fields. See [Artifact Components](/visual-builder/structured-outputs/artifact-components) for more details.

## Adding team agents

Team agents are agents that are part of the same project. You can add team agents to your Agent by dragging and dropping the **Team Agent** block from the top left onto the Canvas.
You can configure custom headers to send on every request to the team agent by clicking on the team agent box and then populating the **Headers** field with your custom headers.

## Adding external agents

External agents let you integrate agents built outside of Inkeep (using other frameworks or platforms) into your Agent.
You can add an external agent to your Agent by dragging and dropping the **External Agent** block from the top left onto the Canvas.

You can configure custom headers to send on every request to the external agent by clicking on the external agent box and then populating the **Headers** field with your custom headers.


# Test Suites (/visual-builder/test-suites)

Build fixed test cases, run agents against them, and evaluate outputs using test suites in the Visual Builder



A **test suite** is a named collection of **items**: each item supplies the messages sent to an agent (the **input**) and optionally the **expected output** you want to compare against. In the Visual Builder, test suites appear under **Test Suites** in the project sidebar.

Test suite runs execute those items against one or more agents, create conversations from each item, and can attach **evaluators** to score the results.

When you start a run with **evaluators** selected, the platform also creates a **batch evaluation** job scoped to that run’s conversations.

## Where to find test suites

1. Open your project in the Visual Builder.
2. In the project sidebar, choose **Test Suites**.

<Note>
  You need **Edit** permission on the project to create or change test suites, items, run configurations, and to start runs. See [Access control](/visual-builder/access-control).
</Note>

## Create a test suite

From the **Test Suites** list, create a new suite and give it a **name**. The suite is empty until you add items.

## Test suite items

Each item has:

* **Input** — JSON object with a `messages` array. Each message has a `role` (`user`, `assistant`, or `system`) and `content` in the same shape as chat messages elsewhere in the product (for example `text` strings, or `parts` for richer content). This is what the agent sees when the item is run.
* **Expected output** (optional) — JSON array of messages with the same role/content shape. Use it to record the reference reply you care about; evaluators or your own tooling can compare model output to this.

<img src="/images/test-suite-item.png" alt="Create Test Suite Item dialog showing input messages and optional expected output" style={{ borderRadius: '8px' }} />

### Bulk upload from CSV

From the **Items** tab on a test suite, choose **Upload CSV** to create many items at once. The file must be UTF-8 and include a header row.

Recognized headers (case-insensitive):

* **input** (required)
* **expectedOutput** (optional)

Each cell can contain either:

* **Plain text** — a single-turn input becomes `{ messages: [{ role: 'user', content: '<text>' }] }`, and a plain-text expected output becomes `[{ role: 'assistant', content: '<text>' }]`.
* **JSON** — paste the full `input`/`expectedOutput` shape directly when you need multi-turn conversations, system prompts, or structured `parts` content.

```csv
input,expectedOutput
"What is 2+2?","4"
"{""messages"":[{""role"":""system"",""content"":""Be terse.""},{""role"":""user"",""content"":""Ping""}]}","[{""role"":""assistant"",""content"":""Pong""}]"
```

Rows with missing `input` or invalid JSON are skipped and reported before upload so you can fix them and re-try. Successfully parsed rows are sent to `POST .../dataset-items/{datasetId}/items/bulk` in a single request.

## Agents

You can **link agents to a test suite** so you can filter or scope which agents are associated with that suite (for example when choosing who runs the items). Run configurations still declare which agents actually execute a given run.

## Run configurations and runs

A **run configuration** ties a test suite to:

* One or more **agents** that will each process every item (each item × agent produces a run invocation).
* Optional **evaluators** to run on the resulting conversations.

Create a run configuration from the test suite detail page (**Runs** tab). When you **start a run**, the platform creates a **test suite run** and processes items. You need at least one item and at least one agent on the configuration before a run can start.

<img src="/images/test-suite-run.png" alt="Create Test Suite Run dialog showing name, description, agent selection, and optional evaluators" style={{ borderRadius: '8px' }} />

Open a run to see per-item invocations, conversation links, and evaluation output when evaluators are configured.

### Rerun a past run

Each row on the **Runs** tab has a **Rerun** action, and the run detail page exposes a **Rerun** button in the header. Triggering a rerun creates a new row in the runs table using:

* The **current** items in the test suite (any items you've added since the source run are included; deleted items are skipped).
* The **same run configuration** (agents, name) as the source run.
* The **same evaluators** that were attached to the source run, unless you pass overrides.

The rerun endpoint is `POST .../dataset-runs/{runId}/rerun`. The response includes the new `datasetRunId` so you can navigate straight to the in-progress run. Runs that were not created from a run configuration can't be rerun — the button is disabled for those rows.

## Programmatic access

| Surface                                                    | Use for                                                                                                                             |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| [Evaluations API reference](/api-reference/evaluations)    | CRUD test suites and items, agent links, run configs, trigger runs (`POST .../dataset-run-configs/{id}/run`), list runs and results |
| [TypeScript SDK: Evaluations](/typescript-sdk/evaluations) | `EvaluationClient` helpers (`listDatasets`, `createDataset`, `createDatasetItem`, `createDatasetItems`, etc.)                       |

Listing test suites supports an optional `agentId` query parameter on the list endpoint to restrict results to suites linked to that agent.

## Related

* [Evaluations in Visual Builder](/visual-builder/evaluations) — Evaluators, batch evaluations over conversations, and continuous tests
* [Evaluations API reference](/api-reference/evaluations)
* [TypeScript SDK: Evaluations](/typescript-sdk/evaluations)


# Environment Configuration (/community/contributing/environment-configuration)

How to configure the environment variables for the Inkeep Agent Framework



## Overview

The Inkeep Agents framework uses a **centralized environment configuration**. This approach provides a single source of truth for all environment variables across the monorepo, eliminating duplication and simplifying configuration management.

## Configuration Structure

### Single Root Configuration

All packages in the monorepo reference a **single `.env` file** at the repository root. This is different from the typical approach of having separate `.env` files per package.

```
agents-4/
├── .env                    # Main configuration (gitignored)
├── .env.example            # Template with all variables
└── packages/
    └── agents-core/
        └── src/env.ts      # Centralized env loader
```

### Loading Priority

Environment variables are loaded in the following order (highest priority first):

1. **`/package-name/.env`** Package specific configuration
2. **`.env`** - Main configuration file
3. **`~/.inkeep/config`** - User-global settings (shared across all repo copies)
4. **`.env.example`** - Default values

This hierarchy allows for flexible configuration management across different scenarios. If you have .env or .env.local in a package directory, they will override the root .env or .env.local for that package.

## Use Cases

### 1. Basic Local Development

For local development, `pnpm setup-dev` creates your `.env` and initializes databases. You only need to add your AI provider API keys:

```bash
# Run setup (creates .env, starts databases, runs migrations)
pnpm setup-dev

# Add your API keys to .env
vim .env

# Start development
pnpm dev
```

## Troubleshooting

### Auto-login not working

The Manage UI auto-logs you in during local development via a server-side proxy (`proxy.ts`). If you're redirected to the login page instead, your `.env` is likely missing the required variables. Ensure these are set:

```bash
INKEEP_AGENTS_MANAGE_UI_USERNAME=admin@example.com
INKEEP_AGENTS_MANAGE_UI_PASSWORD=<generated by setup-dev>
BETTER_AUTH_SECRET=<generated by setup-dev>
INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET=<any-shared-secret>
```

Then run `pnpm db:auth:init` to create the admin user, and restart `pnpm dev`. Check the server console for `[proxy]` messages if the issue persists.

If you recently clicked "Log out", a `dev-logged-out` cookie prevents auto-login so you can test login flows. Clear your cookies or restart your browser to restore auto-login.

<Note>
  If you used `pnpm setup-dev`, these variables are automatically generated with secure values and the admin user is created automatically.
</Note>

### Environment variables not loading

1. Check loading order - later sources override earlier ones
2. Verify file paths are correct
3. Ensure `packages/agents-core` is built: `pnpm --filter @inkeep/agents-core build`

### Missing variables in production

Ensure all required variables are set in your deployment environment. The application will fail fast if critical variables are missing.

### Database connection issues

* For SQLite: Make sure you are using an absolute path to the db file


# Isolated Development Environments (/community/contributing/isolated-environments)

Run multiple parallel dev environments with zero port conflicts



## Overview

The default `pnpm setup-dev` uses fixed host ports (5432, 5433, etc.), so only one dev stack can run at a time. Isolated environments remove this limitation — each instance gets its own Docker containers, volumes, and network with dynamically assigned ports.

Use cases:

* Working on multiple features side-by-side
* Running parallel AI agent sessions (Claude Code, Ship)
* Avoiding port conflicts with teammates or other Docker projects

<Tip>
  The default `pnpm setup-dev` workflow is unchanged. Isolated environments are opt-in — use them only when you need parallelism.
</Tip>

## Quick start

```bash
# 1. Create a fully provisioned isolated environment
pnpm setup-dev --isolated feature-auth

# 2. Point your shell at it
source <(./scripts/isolated-env.sh env feature-auth)

# 3. Run the app against the isolated backing services
pnpm dev

# 4. Tear down when done
./scripts/isolated-env.sh down feature-auth
```

## Commands

| Command                                  | Description                                                        |
| ---------------------------------------- | ------------------------------------------------------------------ |
| `./scripts/isolated-env.sh setup <name>` | Start containers, run migrations, initialize auth - full dev setup |
| `./scripts/isolated-env.sh up <name>`    | Start containers only (no migrations)                              |
| `./scripts/isolated-env.sh down <name>`  | Stop containers and remove volumes                                 |
| `./scripts/isolated-env.sh status`       | List all running isolated environments with their ports            |
| `./scripts/isolated-env.sh env <name>`   | Print shell-exportable environment variables for an instance       |

The `<name>` can be anything descriptive — a feature name, ticket ID, or branch name (e.g., `feature-auth`, `prd-6168`, `billing-v2`).

## How it works

Each isolated environment is a separate Docker Compose project:

1. **`COMPOSE_PROJECT_NAME=agents-<name>`** namespaces all containers, volumes, and networks in Docker
2. **Dynamic port allocation** — services expose container ports without binding to specific host ports, so Docker assigns random available ones
3. **Port discovery** — after startup, the script queries Docker for the assigned ports and saves them to `.isolated-envs/<name>.json`
4. **App port allocation** — free host ports are found for `agents-api` and `agents-manage-ui` via python socket binding
5. **Environment export** — `env <name>` reads the state file and exports database URLs, app ports (`AGENTS_API_PORT`, `MANAGE_UI_PORT`), and API URLs (`INKEEP_AGENTS_API_URL`) so both backing services and the app layer use isolated ports

The backing services are the same as `pnpm setup-dev`: Doltgres (manage DB), Postgres (runtime DB), SpiceDB + its Postgres (authorization).

## Environment variables

Running `source <(./scripts/isolated-env.sh env <name>)` exports:

```text
# Backing service connections
INKEEP_AGENTS_MANAGE_DATABASE_URL=postgresql://appuser:password@localhost:<doltgres-port>/inkeep_agents
INKEEP_AGENTS_RUN_DATABASE_URL=postgresql://appuser:password@localhost:<postgres-port>/inkeep_agents
SPICEDB_ENDPOINT=localhost:<spicedb-grpc-port>

# App ports (so parallel pnpm dev sessions don't collide)
AGENTS_API_PORT=<random-port>
MANAGE_UI_PORT=<random-port>

# API URL overrides (so manage-ui reaches the correct agents-api)
INKEEP_AGENTS_API_URL=http://localhost:<agents-api-port>
PUBLIC_INKEEP_AGENTS_API_URL=http://localhost:<agents-api-port>
NEXT_PUBLIC_INKEEP_AGENTS_API_URL=http://localhost:<agents-api-port>
```

These override the values in your `.env` file for the current shell session. Run `pnpm dev` in the same shell to connect to the isolated instance.

You can also set app ports manually without using isolated environments:

```bash
AGENTS_API_PORT=4002 MANAGE_UI_PORT=4000 pnpm dev
```

## Running multiple environments

```bash
# Start two environments
./scripts/isolated-env.sh setup feature-auth
./scripts/isolated-env.sh setup feature-billing

# Check what's running
./scripts/isolated-env.sh status

# Work on one
source <(./scripts/isolated-env.sh env feature-auth)
pnpm dev

# Switch to the other (in a new terminal)
source <(./scripts/isolated-env.sh env feature-billing)
pnpm dev
```

Each environment is fully independent — separate databases, separate data, separate ports.

## Cleanup

```bash
# Remove a specific environment
./scripts/isolated-env.sh down feature-auth

# Check what's still running
./scripts/isolated-env.sh status
```

`down` removes containers **and** volumes, so all data is deleted. If you just want to stop containers and keep data, use `docker compose -p agents-<name> stop` directly.

<Note>
  Each isolated environment gets its own backing services (databases, authorization) and app ports. Running `source <(./scripts/isolated-env.sh env <name>)` before `pnpm dev` ensures both the backing services and the application layer (`agents-api`, `agents-manage-ui`) use isolated ports with zero collisions.
</Note>


# Working with the manage database (Doltgres) (/community/contributing/manage-database)

How branch-scoped database connections work in the Inkeep Agent Framework



## Overview

The management database is a [Doltgres](https://docs.doltgres.com/) instance — a PostgreSQL-compatible database with Git-like versioning built in.

## Branch naming convention

Each project gets its own main branch, namespaced as:

```
<tenantId>_<projectId>_main
```

For example, a project with `tenantId: "acme"` and `projectId: "proj_123"` has a main branch named `acme_proj_123_main`.

Connections to doltgres default to the `main` branch, so it is critical that **every connection checks out the correct project branch before performing any operations**. The framework provides two mechanisms for this depending on where your code runs.

## Mechanism 1: middleware (manage routes)

All `/manage` API routes pass through the `branchScopedDbMiddleware`. This middleware:

1. Acquires a dedicated connection from the pool
2. Checks out the correct branch (resolved from the request's `ref` query parameter)
3. Creates a Drizzle ORM client scoped to that connection
4. Injects it into the Hono context as `'db'`
5. Executes the route handler
6. Auto-commits changes on success for write operations (POST, PUT, PATCH, DELETE)
7. Cleans up: checks out `main` and releases the connection

### Usage in manage routes

Inside any `/manage` route handler, retrieve the branch-scoped database client from context:

```typescript
async (c) => {
  const db: AgentsManageDatabaseClient = c.get('db');
  const { tenantId, projectId } = c.req.valid('param');

  const tools = await listTools(db)({
    scopes: { tenantId, projectId },
  });

  return c.json({ data: tools });
}
```

## Mechanism 2: `withRef`

When you need to read or write manage data from outside the `/manage` routes — for example from the `/run` domain, `/evals` domain, or other packages — use the `withRef` wrapper from `@inkeep/agents-core`.

`withRef` handles the full lifecycle of a branch-scoped connection:

1. Acquires a dedicated connection from the pool
2. Checks out the specified branch
3. Executes your callback with a scoped Drizzle client
4. Optionally auto-commits on success for write operations
5. Cleans up: checks out `main`, releases the connection

### Read example

```typescript
import { withRef } from '@inkeep/agents-core';
import manageDbPool from '../data/db/manageDbPool';

const agent = await withRef(manageDbPool, resolvedRef, (db) =>
  getFullAgent(db)({
    scopes: { tenantId, projectId, agentId },
  })
);
```

### Write example with auto-commit

```typescript
await withRef(
  manageDbPool,
  resolvedRef,
  async (db) => {
    await updateAgent(db, agentId, data);
  },
  { commit: true, commitMessage: 'Update agent config' }
);
```

### Batching multiple operations

You can batch multiple reads or writes in a single `withRef` call. This uses one connection and one branch checkout for all operations:

```typescript
const result = await withRef(
  manageDbPool,
  resolvedRef,
  async (db) => {
    await createCredential(db, credData);
    await updateTool(db, toolId, { credentialId });
    return { success: true };
  },
  {
    commit: true,
    commitMessage: 'Link credential to tool',
    author: { name: 'oauth-flow', email: 'oauth@inkeep.com' },
  }
);
```

### Obtaining a `resolvedRef`

Most runtime code paths already have a `resolvedRef` available — either through the Hono context (`c.get('resolvedRef')`) or via an execution context (`this.executionContext.resolvedRef`).
If you need to resolve a ref manually, use `getProjectScopedRef` and `resolveRef`:

```typescript
import { getProjectScopedRef, resolveRef } from '@inkeep/agents-core';

const projectScopedRef = getProjectScopedRef(tenantId, projectId, 'main');

// You can directly use a manage db client to resolve the ref
const resolvedRef = await resolveRef(agentsManageDbClient)(projectScopedRef);
```

## Decision guide

| Where is your code?                 | What to use                                                |
| ----------------------------------- | ---------------------------------------------------------- |
| `/manage` route handler             | `c.get('db')` — the middleware handles checkout and commit |
| `/run` or `/evals` route handler    | `withRef(manageDbPool, resolvedRef, (db) => ...)`          |
| Service class or utility            | `withRef(manageDbPool, resolvedRef, (db) => ...)`          |
| Already inside a `withRef` callback | Use the `db` from the callback argument directly           |

## Key files

| File                                                  | Purpose                                                                 |
| ----------------------------------------------------- | ----------------------------------------------------------------------- |
| `agents-api/src/middleware/branchScopedDb.ts`         | Middleware for `/manage` routes                                         |
| `packages/agents-core/src/dolt/ref-scope.ts`          | `withRef` wrapper and nesting detection                                 |
| `packages/agents-core/src/dolt/ref-middleware.ts`     | Ref resolution middleware (resolves `ref` query param to `ResolvedRef`) |
| `packages/agents-core/src/dolt/ref-helpers.ts`        | Helper functions for ref resolution and management                      |
| `packages/agents-core/src/validation/dolt-schemas.ts` | `ResolvedRef` type and Zod schemas                                      |
| `agents-api/src/data/db/manageDbPool.ts`              | The connection pool singleton                                           |


# Contribute to Inkeep Open Source project (/community/contributing/overview)

How to contribute to the Inkeep Agent Framework



Thank you for your interest in contributing to the Agent Framework! This document provides guidelines and information for contributors.

## Launch the Visual Builder

### Prerequisites

Before getting started, ensure you have the following installed on your system:

* [Node.js](https://nodejs.org/en/download/) version 22.18 or higher (matches `.node-version`)
* [Docker](https://docs.docker.com/get-docker/)
* [pnpm](https://pnpm.io/installation) version 10.30.0 or higher (matches `packageManager`)

### Step 1: Clone the repository

```
git clone https://github.com/inkeep/agents.git
cd agents
```

### Step 2: Run the setup script

Ensure Docker Desktop (or Docker daemon) is running, then run:

```bash
pnpm setup-dev
```

This will:

* Create `.env` from `.env.example` if missing
* Generate secrets (JWT keys, signing secret, auth secret, UI password) if not already configured
* Check that Docker is running and required ports (5432, 5433, 5434) are free
* Start Doltgres (manage) + Postgres (runtime) + SpiceDB (authorization) via `docker-compose.dbs.yml`
* Run database migrations (manage + runtime)
* Initialize the default organization and admin user
* Push a sample project (activities-planner) so the UI is pre-seeded

<Tip>
  Safe to re-run at any time. Each step is additive — it won't drop tables, delete projects, or reset credentials. Migrations apply only what's new, and existing data is preserved. If Docker containers are already running, setup skips startup and proceeds to migrations.
</Tip>

If another Docker Compose project is using the required ports, setup tells you exactly which project is blocking and the command to stop it.

Need to run multiple dev environments in parallel (e.g., working on two features, or running AI agent sessions side-by-side)? See [Isolated Environments](/community/contributing/isolated-environments).

Add API keys for the AI providers you want to use in the root `.env` or `~/.inkeep/config`. `ANTHROPIC_API_KEY` is required.

```dotenv
ANTHROPIC_API_KEY=sk-ant-xyz789
OPENAI_API_KEY=sk-xxx # optional
GOOGLE_GENERATIVE_AI_API_KEY=sk-xxx # optional
```

### Step 3: Run the agent framework

```bash
pnpm dev
```

### Step 4: Start building!

Open `http://localhost:3000` in the browser — you'll be signed in automatically. Start building agents right away.

## Push your project

A key advantage of the Inkeep agent framework is its seamless code-to-visual workflow: define your agents programmatically, push them to the Visual Builder, and continue developing with the intuitive drag-and-drop interface.

Follow the steps below to push your project using the [Inkeep CLI](/typescript-sdk/cli-reference).

### Step 1: Download the Inkeep CLI

```bash
pnpm add -g @inkeep/agents-cli
```

### Step 2: Push the project

```bash
cd agents-cookbook/template-projects/activities-planner
inkeep push
```

<Note>
  The CLI reads `tenantId` and API URLs from the nearest `inkeep.config.ts` (it searches parent
  directories). The cookbook templates default to `tenantId: 'default'` and `agentsApi.url:
    'http://localhost:3002'`. Update `agents-cookbook/template-projects/inkeep.config.ts` if needed.
</Note>

### Step 3: Observe the Agent in the Visual Builder

<Image src="/gifs/basic-agent.gif" alt="Basic Agent" style={{ border: "1px solid #e1e5e9" }} />

## Set up optional services (traces + credentials)

Set up all optional local services — Nango (credential store), SigNoz (traces), OTEL Collector, and Jaeger — with one command:

```bash
pnpm setup-dev:optional
```

This clones [`agents-optional-local-dev`](https://github.com/inkeep/agents-optional-local-dev) into `.optional-services/`, starts services via Docker Compose, and writes the required env vars to your `.env`. Restart `pnpm dev` to pick up the new variables.

* **Traces**: Open `http://localhost:3080` for SigNoz. See [Traces](/get-started/traces) for details.
* **Credentials**: Create a credential in the Visual Builder using Nango Store. See [Nango](/typescript-sdk/credentials/nango) for details.

Use `pnpm optional:status` to check service health, `pnpm optional:stop` to stop optional services, or `pnpm optional:reset` to start fresh.

<Image src="/images/live-traces.png" alt="Live traces showing" style={{ border: "1px solid #e1e5e9" }} />

## Development Workflow

### Conductor

The repo includes a `conductor.json` that automatically configures Claude Code workspaces with the correct permissions and skills. No manual setup needed.

### Git Hooks

This project uses Husky for Git hooks to maintain code quality. The hooks are automatically installed when you run `pnpm install`.

#### Pre-commit Hook

The pre-commit hook runs `pnpm lint-staged`, which:

* Formats and lints staged files with Biome
* Runs `agents-api` tests for affected changes

#### Pre-push Hook

The pre-push hook runs `pnpm format` and validates changeset package names before pushing. If a changeset references an invalid workspace package, the push will fail with an error listing valid package names.

##### Bypassing Checks

While we recommend running all checks, there are legitimate cases where you might need to bypass hooks:

```bash
git commit --no-verify -m "emergency: hotfix for production"
```

> **Note:** Use these bypass mechanisms sparingly. They're intended for:
>
> * Work-in-progress commits that you'll fix before pushing
> * Emergency fixes where speed is critical
> * Commits that only touch non-code files (though hooks are smart enough to handle this)

### Local checks (CI parity)

* `pnpm check` mirrors the full CI merge gate: lint, typecheck, tests, format verification, env-description validation, dead code detection (knip), and changeset package name validation.
* For package-specific commands, see `AGENTS.md` below.

## CLI Development

## Running the CLI Locally

When developing the CLI, you can run the local version directly without global installation:

```bash
# Build the CLI first
cd agents-cli
pnpm build

# Run the local CLI from the repository root
node agents-cli/dist/index.js --version
```

## Testing CLI Changes

1. **Build the CLI after making changes:**

```bash
cd agents-cli
pnpm build
```

2. **Test commands locally:**

```bash
# From repository root
node agents-cli/dist/index.js push agents-cookbook/template-projects/activities-planner
node agents-cli/dist/index.js --help
```

3. **Run CLI tests:**

```bash
cd agents-cli
pnpm test
```

## Switching Between Local and Published CLI

During development, you may need to test both the local development version and the published npm package:

```bash
# Use local development version
node /path/to/agents/agents-cli/dist/index.js --version

# Use globally installed published version
inkeep --version
```

For more CLI setup options, see the CLI docs:
[https://docs.inkeep.com/guides/cli/overview](https://docs.inkeep.com/guides/cli/overview)

## Commit Messages

We recommend conventional commit format (not enforced by tooling):

```
type(scope): description

[optional body]

[optional footer]
```

Types:

* `feat`: New feature
* `fix`: Bug fix
* `docs`: Documentation changes
* `style`: Code style changes (formatting, etc.)
* `refactor`: Code refactoring
* `test`: Test changes
* `chore`: Build process or auxiliary tool changes

## Pull Requests

1. Fork the repository
2. Create a feature branch (`git checkout -b feat/amazing-feature`)
3. Make your changes
4. Ensure `pnpm check` passes (mirrors CI; see below)
5. Commit your changes (following commit message guidelines)
6. Push to your fork
7. Open a pull request

### PR Guidelines

* Keep PRs focused on a single feature or fix
* Update tests for any behavior changes
* Update documentation as needed
* Ensure CI passes before requesting review

## Continuous Integration

Our CI pipeline runs on all pull requests and includes:

* `pnpm check` (lint, typecheck, tests, format verification, env-description validation, knip, changeset package name validation)
* Create Agents E2E tests (separate job)

These checks must pass before a PR can be merged. Pre-commit uses lint-staged, pre-push runs `pnpm format` and changeset validation, and CI is the source of truth.

### Migration Lineage Check

PRs that touch migration files in `packages/agents-core/drizzle/manage/` or `packages/agents-core/drizzle/runtime/` are automatically validated by a CI guard (`scripts/check-migration-lineage.mjs`). The check is skipped for PRs that don't modify these directories.

The script verifies that migrations are **append-only** and **immutable**, following the lineage from `main`:

* The current journal must start with the exact journal from `main` (no rewriting history)
* Each historical journal entry must match the full content from `main` (not just the tag)
* Every existing migration SQL file on `main` must still exist and have identical content
* New journal entries must be appended at the end
* The first new migration number must be the immediate next after `main`
* Every new SQL file must belong to the newly appended journal tail
* No new SQL file may reuse a migration number already present on `main`

If the check fails with a **"rebase and regenerate"** error, your branch has stale migrations that conflict with newer ones on `main`. To fix this, rebase your branch on `main` and regenerate your migrations with the appropriate `pnpm db:generate` command.

### Testing Dolt schema migrations across branches

When you change the manage-schema, you should verify that the schema change can be merged from the `main` schema branch into existing project branches containing real data.
Doltgres is still in beta and there may be discrepancies between valid postgres and doltgres behavior. These discrepancies may cause migrations to fail when merged into project branches.

#### Recommended workflow

1. Start from a local database that already has projects with representative test data.
2. Apply the new manage migration:

```bash
pnpm db:manage:migrate
```

3. Run the branch sync script to merge the schema change from `main` into every project branch:

```bash
pnpm --filter @inkeep/agents-core db:manage:sync-all-branches
```

4. Inspect the output. Each branch should report either:
   * `SYNCED` if the schema changed successfully
   * `NOOP` if the branch was already up to date
5. Re-open the branches you care about and verify the migration still works against real data. At minimum, check the tables or code paths affected by the migration and confirm reads, writes, and merges behave as expected.

#### Backfill migrations

Data backfills will not be applied when merging schema changes into project branches. If your migration requires a data backfill, run it manually after merging schema changes:

```bash
pnpm --filter @inkeep/agents-core db:manage:run-backfill-all-branches --file <path-to-backfill-sql>
```

## AI coding assistants and coding practices

Many of our best coding practices for AI assistants live in `AGENTS.md`. We render it below for reference.

* Source file: [https://github.com/inkeep/agents/blob/main/AGENTS.md](https://github.com/inkeep/agents/blob/main/AGENTS.md)

<>
  # AGENTS.md - Comprehensive Guide for AI Coding Agents

  This file provides guidance for AI coding agents (Claude Code, Cursor, Codex, Amp, etc.) when working with code in this repository.

  ## Essential Commands - Quick Reference

  ### Build & Development

  * **Build**: `pnpm build` (root) or `turbo build`
  * **Dev**: `pnpm dev` (root) or navigate to package and run `pnpm dev`
  * **Setup (core)**: `pnpm setup-dev` — core DBs (Doltgres, Postgres, SpiceDB), env config, migrations, admin user
  * **Setup (isolated)**: `pnpm setup-dev --isolated <name>` — same as above but in a parallel environment (see [Isolated Environments](#isolated-parallel-environments))
  * **Setup (optional services)**: `pnpm setup-dev:optional` — Nango + SigNoz + OTEL + Jaeger (run `setup-dev` first)
  * **Optional services lifecycle**: `pnpm optional:stop` | `pnpm optional:status` | `pnpm optional:reset`

  ### Verification

  **Pre-push** (run both, in order):

  ```bash
  pnpm format     # auto-fix formatting
  pnpm check      # lint + typecheck + test + format:check + env-descriptions + route-handler-patterns + dal-boundary + knip
  ```

  **Single-command iteration:** `pnpm typecheck`, `pnpm lint` (`lint:fix`), `pnpm test`, `cd <pkg> && pnpm test --run <file>`

  ### Database Operations (run from monorepo root)

  * **Generate migrations**: `pnpm db:generate` - Generate Drizzle migrations from schema changes
  * **Apply migrations**: `pnpm db:migrate` - Apply generated migrations to database
  * **Drop migrations**: `pnpm db:drop` - Drop migration files (use this to remove migrations, don't manually delete)
  * **Database studio**: `pnpm db:studio` - Open Drizzle Studio for database inspection
  * **Check schema**: `pnpm db:check`
  * **Initialize auth**: `pnpm db:auth:init` - Create default organization and admin user for local development

  ### Creating Changelog Entries (Changesets)

  Create a changeset for any user-facing change to a published package:

  ```bash
  pnpm bump <patch|minor|major> --pkg <package> "<message>"
  ```

  **Examples:**

  ```bash
  # Single package
  pnpm bump patch --pkg agents-core "Fix race condition in agent message queue"

  # Multiple packages (for tightly coupled changes)
  pnpm bump minor --pkg agents-sdk --pkg agents-core "Add streaming response support"
  ```

  **Valid package names:** `agents-cli`, `agents-core`, `agents-api`, `agents-manage-ui`, `agents-work-apps`, `agents-sdk`, `create-agents`, `ai-sdk-provider`

  **Semver guidance:**

  * **Major**: Reserved - do not use without explicit approval
  * **Minor**: Schema changes requiring migration, significant behavior changes
  * **Patch**: Bug fixes, additive features, non-breaking changes

  #### Writing Good Changelog Messages

  **Target audience:** Developers consuming these packages

  **Style requirements:**

  * Sentence case: "Add new feature" not "add new feature"
  * Start with action verb: Add, Fix, Update, Remove, Improve, Deprecate
  * Be specific about what changed and why it matters to consumers
  * Keep to 1-2 sentences

  **Good examples:**

  * "Fix race condition when multiple agents connect simultaneously"
  * "Add `timeout` option to `createAgent()` for custom connection timeouts"
  * "Remove deprecated `legacyMode` option (use `mode: 'standard'` instead)"
  * "Improve error messages when agent registration fails"

  **Bad examples:**

  * "fix bug" (too vague - which bug? what was the impact?)
  * "update dependencies" (not user-facing, doesn't need changeset)
  * "Refactored the agent connection handler to use async/await" (implementation detail, not user impact)
  * "changes" (meaningless)

  **When to create a changeset (MANDATORY):**

  * Any bug fix, feature, or behavior change to a published package — even if the package is "internal-facing" (e.g., `agents-work-apps`, `agents-api`). If the code ships to users or affects runtime behavior, it needs a changeset.
  * This includes work-app integrations (Slack, GitHub), API route changes, SDK changes, CLI changes, and core library changes.

  **When NOT to create a changeset:**

  * Documentation-only changes
  * Test-only changes
  * Internal tooling/scripts changes
  * Changes to ignored packages (agents-ui, agents-docs, cookbook-templates, test-agents)

  **Multiple changes in one PR:**
  If a PR affects multiple packages independently, create separate changesets for each with specific messages. If changes are tightly coupled (e.g., updating types in core that SDK depends on), use a single changeset listing both packages.

  ### Running Examples / Reference Implementations

  * Use `agents-cookbook/` for reference implementations and patterns.
  * There is no `examples/` directory; prefer cookbook recipes or package-specific README files.

  ### Documentation Development

  ```bash
  # From agents-docs directory
  pnpm dev              # Start documentation site (port 3000)
  pnpm build           # Build documentation for production
  ```

  ## Code Style (Biome enforced)

  * **Imports**: Use type imports (`import type { Foo } from './bar'`), organize imports enabled, barrel exports (`export * from './module'`)
  * **Formatting**: Single quotes, semicolons required, 100 char line width, 2 space indent, ES5 trailing commas
  * **Types**: Explicit types preferred, avoid `any` where possible (warning), use Zod for validation
  * **Naming**: camelCase for variables/functions, PascalCase for types/components, kebab-case for files
  * **Error Handling**: Use try-catch, validate with Zod schemas, handle errors explicitly
  * **React Compiler**: React Compiler is enabled for this repo. Do not add `memo`, `useMemo`, or `useCallback`; rely on the compiler unless a maintainer explicitly requests an exception
  * **Function Arguments**: When a function has more than 3 parameters, prefer a single object argument so that parameters are well-labeled, ordering doesn't matter, and callers can benefit from spread operators
  * **No Comments**: Do not add comments unless explicitly requested

  ## TypeScript Configuration

  All TypeScript packages under `public/agents/packages/` and `public/agents/agents-*/` should extend the shared strict baseline at `public/agents/tsconfig.base.json`. This file defines the monorepo's strictness contract: `strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `forceConsistentCasingInFileNames`, `skipLibCheck`, `esModuleInterop`, `resolveJsonModule`, `isolatedModules`, `moduleDetection: force`, and `verbatimModuleSyntax: false`.

  ### New packages

  When creating a new workspace package under `public/agents/packages/<name>/`, the `tsconfig.json` should extend the base:

  ```json
  {
    "extends": "../../tsconfig.base.json",
    "compilerOptions": {
      "target": "ES2022",
      "module": "ESNext",
      "moduleResolution": "bundler",
      "outDir": "./dist",
      "rootDir": "./src",
      "declaration": true,
      "sourceMap": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "dist"]
  }
  ```

  Only override target/module/output settings in the per-package config. Strictness flags live in the base; do not duplicate them.

  ### Why this matters

  * Enforces a consistent strictness floor across all TS packages (prevents drift over time)
  * Strictness flags catch real runtime bugs at compile time (e.g., optional-prop-spread issues caught 3 real bugs in `agents-email` during the pilot)
  * Bumping strictness later = edit one file instead of every package's tsconfig
  * New packages inherit the baseline automatically

  ### Opt-out

  A package that legitimately cannot extend the base (e.g., needs older target, needs non-strict mode for generated code) should:

  1. Omit the `extends` line
  2. Add a top-level comment explaining why: `// Opt-out rationale: <reason>`
  3. Self-contain the compiler options it actually needs

  Unexplained opt-outs drift the monorepo's strictness contract and should be caught in review.

  ### Path notes

  The base lives at `public/agents/tsconfig.base.json`. After Copybara mirrors the subtree to `inkeep/agents`, the base lives at `/tsconfig.base.json` on the public repo (prefix stripped). The relative `extends` path from `packages/<name>/tsconfig.json` is `../../tsconfig.base.json` in both repos, portable across the mirror boundary.

  ## Testing (Vitest)

  * Place tests in `__tests__/` directories adjacent to code
  * Name: `*.test.ts` or `*.spec.ts`
  * Pattern: `import { describe, it, expect, beforeEach, vi } from 'vitest'`
  * Run with `--run` flag to avoid watch mode
  * 60-second timeouts for A2A interactions
  * Each test worker uses an embedded Postgres (pglite) database with manage/run Drizzle migrations applied in setup

  ### Visual Regression Tests

  Browser-based visual tests (screenshot comparisons) run Chromium inside a Docker container so screenshots are identical across macOS and Linux CI.

  ```bash
  # Start the Playwright Docker server (one-time, stays running)
  docker compose -f docker-compose.visual.yml up -d

  # Run visual tests
  pnpm --filter @inkeep/agents-manage-ui test:visual

  # Update baselines after intentional UI changes
  pnpm --filter @inkeep/agents-manage-ui test:visual:update

  # Stop the server when done
  docker compose -f docker-compose.visual.yml down
  ```

  * Visual test files: `*.browser.test.tsx`
  * Baselines stored in `src/__screenshots__/`
  * Docker is **required** — without it, tests fall back to local Chromium and screenshots won't match CI

  ## Package Manager

  * Always use `pnpm` (not npm, yarn, or bun)

  ### pnpm-lock.yaml Resolution Strategy

  ⚠️ **NEVER delete `pnpm-lock.yaml` and regenerate it from scratch.** Deleting the lockfile and running `pnpm install` allows the resolver to pick different (often lower) versions of transitive dependencies, causing silent downgrades that break tests or change runtime behavior.

  **Correct approach when the lockfile has merge conflicts or needs updating:**

  1. **Start from the base branch's lockfile** — check out the `pnpm-lock.yaml` from the target base (usually `main`):
     ```bash
     git checkout main -- pnpm-lock.yaml
     ```

  2. **Re-install to layer your branch's dependency changes on top:**
     ```bash
     pnpm install
     ```
     This preserves all existing resolutions from `main` and only adds/updates what your branch's `package.json` changes require.

  3. **Commit the updated lockfile** with your other changes.

  **Why this matters:** The lockfile pins exact transitive dependency versions. Regenerating from scratch lets the resolver freely re-resolve the entire tree, which can silently pick different versions even when `package.json` ranges haven't changed. Starting from the base lockfile ensures only your intentional changes affect resolution.

  **When changing dependencies**: this monorepo has three lockfiles (root, `public/agents`, and `public/agents/create-agents-template`). Use `pnpm install:all` from the monorepo root to regenerate all three at once — stopping short of that is how `ERR_PNPM_OUTDATED_LOCKFILE` ends up blocking CI or Vercel.

  ## Architecture Overview

  This is the **Inkeep Agent Framework** - a multi-agent AI system with A2A (Agent-to-Agent) communication capabilities. The system provides OpenAI Chat Completions compatible API while supporting sophisticated agent orchestration.

  ### Core Components

  #### Unified API (`agents-api`)

  The `agents-api` package contains all API domains under a single service:

  * **`/domains/manage/`** - Agent configuration, projects, tools, and administrative operations
  * **`/domains/run/`** - Agent execution, conversations, A2A communication, and runtime operations
  * **`/domains/evals/`** - Evaluation workflows, dataset management, and evaluation triggers

  #### Multi-Agent Framework

  #### Database Schema (PostgreSQL + Drizzle ORM)

  * [manage-schema.ts](./packages/agents-core/src/db/manage/manage-schema.ts) - Config tables (Doltgres)
  * [runtime-schema.ts](./packages/agents-core/src/db/runtime/runtime-schema.ts) - Runtime tables (Postgres)

  ## Key Implementation Details

  ### CRUD HTTP Method Conventions (RFC 9110, RFC 5789)

  | Operation        | Method | Path Pattern      | Example               |
  | ---------------- | ------ | ----------------- | --------------------- |
  | Create           | POST   | `/resources`      | `POST /agents`        |
  | Read (list)      | GET    | `/resources`      | `GET /agents`         |
  | Read (single)    | GET    | `/resources/{id}` | `GET /agents/{id}`    |
  | Update (partial) | PATCH  | `/resources/{id}` | `PATCH /agents/{id}`  |
  | Delete           | DELETE | `/resources/{id}` | `DELETE /agents/{id}` |

  * **PATCH** for partial updates (RFC 5789) — canonical method for standard CRUD update operations
  * **PUT** for full-resource replacement, upsert, and set/replace operations (RFC 9110 §9.3.4)
  * **POST** for creates and non-idempotent actions (sync, reconnect, cancel, etc.)
  * **GET** for reads — never mutates state
  * **DELETE** for resource removal

  #### Exceptions: PUT-canonical routes

  The following routes use **PUT as the canonical method** because they perform full-resource replacement or upsert semantics, not partial updates:

  | Route                                     | Reason                                              | OperationId                  |
  | ----------------------------------------- | --------------------------------------------------- | ---------------------------- |
  | `PUT /project-full/{projectId}`           | Upsert — creates or fully replaces a project        | `update-full-project`        |
  | `PUT /agents-full/{agentId}`              | Upsert — creates or fully replaces an agent         | `update-full-agent`          |
  | `PUT /tools/{toolId}/github-access`       | Set/replace — replaces entire GitHub access config  | `set-mcp-tool-github-access` |
  | `PUT /tools/{toolId}/slack-access`        | Set/replace — replaces entire Slack access config   | `set-mcp-tool-slack-access`  |
  | `PUT /projects/{projectId}/github-access` | Set/replace — replaces entire project GitHub access | `set-project-github-access`  |

  These routes still register a PATCH method for backward compatibility (with `x-speakeasy-ignore: true`).

  #### Dual-registration helper

  All update routes register both PUT and PATCH methods using `openapiRegisterPutPatchRoutesForLegacy()` from `agents-api/src/utils/openapiDualRoute.ts`. The legacy method gets `x-speakeasy-ignore: true` and a suffixed operationId (e.g., `update-agent-put`). To find all legacy routes, search for usages of this helper. When legacy methods are no longer needed, remove the helper calls and replace with single `app.openapi()` registrations.

  #### Backward compatibility

  All existing PUT update routes remain functional — they share the same handler as PATCH. New update routes must use PATCH as canonical (or PUT for upsert/set-replace semantics). Do not remove existing PUT routes without a deprecation period.

  ### Database Migration Workflow

  #### Standard Workflow

  1. Edit `packages/agents-core/src/db/manage/manage-schema.ts` or `packages/agents-core/src/db/runtime/runtime-schema.ts`
  2. Run `pnpm db:generate` to create migration files in `drizzle/`
  3. (Optional) Make minor edits to the newly generated SQL file if needed due to drizzle-kit limitations
  4. Run `pnpm db:migrate` to apply the migration to the database

  #### Important Rules

  * ⚠️ **NEVER manually edit files in `drizzle/meta/`** - these are managed by drizzle-kit
  * ⚠️ **NEVER edit existing migration SQL files after they've been applied** - create new migrations instead
  * ⚠️ **To remove migrations, use `pnpm db:drop`** - don't manually delete migration files
  * ✅ **Only edit newly generated migrations** before first application (if drizzle-kit has limitations)

  ### Testing Patterns (MANDATORY for all new features)

  * **Vitest**: Test framework with 60-second timeouts for A2A interactions
  * **Isolated Databases**: Each test worker gets in-memory SQLite database
  * **Integration Tests**: End-to-end A2A communication testing
  * **Test Structure**: Tests must be in `__tests__` directories, named `*.test.ts`
  * **Coverage Requirements**: All new code paths must have test coverage

  #### Shared Test Mocks (`@inkeep/agents-core/test-utils`)

  Use the shared mock factories instead of defining inline mocks. This prevents drift when the real API changes.

  ```typescript
  import { createMockLoggerModule } from '@inkeep/agents-core/test-utils';

  // Simple — just suppress logger noise (most common):
  vi.mock('../../logger', () => createMockLoggerModule().module);

  // When asserting on logger calls:
  const { mockLogger, module: loggerModule, clearAll } = createMockLoggerModule();
  vi.mock('../../logger', () => loggerModule);
  // In beforeEach: clearAll() — vi.clearAllMocks() does not reach nested mock fns

  // Edge case: static imports after vi.mock (TDZ issue) — use vi.hoisted container:
  const refs = vi.hoisted(() => ({ mockLogger: null as any }));
  vi.mock('../../logger', async () => {
    const { createMockLoggerModule } = await import('@inkeep/agents-core/test-utils');
    const result = createMockLoggerModule();
    refs.mockLogger = result.mockLogger;
    return result.module;
  });
  ```

  **Do NOT** define inline logger mocks (`{ info: vi.fn(), warn: vi.fn(), ... }`) — use the factory.

  #### Example Test Structure

  ```typescript
  // src/builder/__tests__/myFeature.test.ts
  import { describe, it, expect, beforeEach, vi } from 'vitest';

  describe('MyFeature', () => {
    beforeEach(() => {
      vi.clearAllMocks();
    });

    it('should handle success case', async () => {
      // Test implementation
    });

    it('should handle error case', async () => {
      // Test error handling
    });
  });
  ```

  ### Environment Configuration

  Required environment variables in `.env` files:

  ```dotenv
  ENVIRONMENT=development|production|test
  INKEEP_AGENTS_MANAGE_DATABASE_URL=postgresql://appuser:password@localhost:5432/inkeep_agents
  INKEEP_AGENTS_RUN_DATABASE_URL=postgresql://appuser:password@localhost:5433/inkeep_agents
  PORT=3002
  ANTHROPIC_API_KEY=required
  OPENAI_API_KEY=optional
  LOG_LEVEL=debug|info|warn|error
  ```

  ### Isolated Parallel Environments

  Run multiple full dev stacks simultaneously with zero port conflicts. Each isolated environment gets its own Docker containers, volumes, and network with dynamically assigned ports.

  **When to use:** Running multiple features in parallel, AI coding agents executing concurrently, or testing against an isolated database without affecting the default environment.

  #### Quick Start

  ```bash
  # Create an isolated environment (Docker + migrations + auth)
  pnpm setup-dev --isolated my-feature

  # Point your shell at it
  source <(./scripts/isolated-env.sh env my-feature)

  # Run the app (uses isolated databases + dynamic app ports)
  pnpm dev

  # Tear down when done
  ./scripts/isolated-env.sh down my-feature
  ```

  #### Available Commands

  | Command                                  | Description                                                 |
  | ---------------------------------------- | ----------------------------------------------------------- |
  | `./scripts/isolated-env.sh setup <name>` | Full setup: Docker + health checks + migrations + auth init |
  | `./scripts/isolated-env.sh up <name>`    | Start containers only (no migrations)                       |
  | `./scripts/isolated-env.sh down <name>`  | Stop and remove containers + volumes                        |
  | `./scripts/isolated-env.sh status`       | List all running isolated environments with ports           |
  | `./scripts/isolated-env.sh env <name>`   | Print source-able env var exports                           |

  #### How It Works

  * Uses `docker-compose.isolated.yml` with `COMPOSE_PROJECT_NAME` for namespace isolation
  * Docker assigns random available host ports (no hardcoded bindings)
  * Ports are discovered post-startup via `docker compose port` and saved to `.isolated-envs/<name>.json`
  * The `env` command outputs `export` statements that override database URLs (`INKEEP_AGENTS_MANAGE_DATABASE_URL`, `INKEEP_AGENTS_RUN_DATABASE_URL`, `SPICEDB_ENDPOINT`) and app ports (`AGENTS_API_PORT`, `MANAGE_UI_PORT`, `INKEEP_AGENTS_API_URL`)
  * Default environment (`docker-compose.dbs.yml` on fixed ports) continues to work unchanged

  #### Key Files

  | File                          | Purpose                                   |
  | ----------------------------- | ----------------------------------------- |
  | `docker-compose.isolated.yml` | Compose file with dynamic port allocation |
  | `scripts/isolated-env.sh`     | CLI for managing isolated environments    |
  | `.isolated-envs/<name>.json`  | Port state files (gitignored)             |

  ## High Product-level Thinking

  This repo is a product with multiple user-facing surfaces and shared contracts. A “small” change in one place can have real side effects elsewhere.

  **Rule:** Do NOT implement additional work beyond what the user requested. If you identify likely cross-surface impacts, **flag them** and **ask** the user to get full clarity and ensure high degree of product-level thinking. You can flag prior to starting new work/tasks, but also if you identify ambiguities or additional things to consider mid-execution.

  **If the request is NOT backed by a PRD, a well-specced artifact, or clearly scoped small task**, pause and ask targeted, highly relevant questions when the blast radius is unclear. When applicable, suggest user make a PRD; offer to help by leveraging the `prd` skill.

  Your goal is to **AVOID**:

  * locking in unintended behavior
  * making changes without thinking through all potential interaction points for how a user may consume or interact with a product change
  * breaking changes or breaking data contracts without clear acknowledgement and plan
  * missing critical dimensions to feautre development relevant, like authorization or security
  * identify any side effects of the work that is being asked
  * implementations that contradict or duplicate existing patterns/abstractions

  Your responsibility is to think through the work that is being done from all dimensions and considerations.

  ### What to clarify (high-signal triggers)

  * **Definition shapes / shared types / validation rules** (agent/project/tool/credential/etc.)
  * **Runtime behavior or streaming formats** (responses, tool calls, artifacts/components)
  * **Tracing / telemetry** (span names, attribute keys, correlation IDs, exporter config)
  * **Resources, endpoints, or actions** (create/update/delete, new capabilities)
  * **Auth / permissions / tenancy** (view/use/edit boundaries, RBAC, fine-grained authz, multi-tenant scoping)

  ### Surface area analysis (load before planning or implementing)

  This product has **50+ customer-facing** and **100+ internal tooling/devops** surfaces with complex dependency chains. When planning or implementing any feature or change, load the relevant surface area skill to map the blast radius and plan "to-dos" of all areas that need to be addressed before writing a line of code:

  | Skill                    | Scope                                                                                                                                                            | Load when                                                                                 |
  | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
  | `audience-impact`        | Who is affected and how fast does it reach them? Maps roles (Contributor, Builder, Platform User) × deployment modes (Cloud, Self-hosted) to impact propagation. | Start here — identifies which audiences a change affects and what deliverables are needed |
  | `product-surface-areas`  | APIs, SDKs, CLI, UIs, Widgets, Event Streams, docs, protocols, templates, etc.                                                                                   | Change affects anything a customer (developer or no-code admin) uses or depends on        |
  | `internal-surface-areas` | Build, CI/CD, DB, auth, runtime engine, test infra, internal AI tooling, etc.                                                                                    | Change affects infrastructure, tooling, or shared internals                               |

  **Tip**: Start with `audience-impact` to quickly identify who cares and how changes propagate, then load the catalog skills for detailed surface tracing. Both catalog skills include dependency graphs, breaking change impact matrices, and transitive chain tracing.

  ## Development Guidelines

  ### ⚠️ MANDATORY: Required for All New Features

  **ALL new work MUST include these three components - NO EXCEPTIONS:**

  ✅ **1. Unit Tests**

  * Write comprehensive unit tests using Vitest
  * Place tests in `__tests__` directories adjacent to the code
  * Follow naming convention: `*.test.ts`
  * Minimum coverage for new code paths
  * Test both success and error cases

  ✅ **2. Agent Builder UI Components**

  * Add corresponding UI components in `/agents-manage-ui/src/components/`
  * Include form validation schemas
  * Update relevant pages in `/agents-manage-ui/src/app/`
  * Follow existing Next.js and React patterns
  * Use the existing UI component library

  ✅ **3. Documentation**

  * Create or update documentation in `/agents-docs/content/docs/` (public-facing docs)
  * Documentation should be in MDX format (`.mdx` files)
  * Follow the **write-docs** skill whenever creating or modifying documentation

  **Before marking any feature complete, verify:**

  * [ ] `pnpm check` passes
  * [ ] **Changeset created** via `pnpm bump` for every published package with runtime behavior changes (see [Creating Changelog Entries](#creating-changelog-entries-changesets))
  * [ ] UI components implemented in agents-manage-ui
  * [ ] Documentation added to `/agents-docs/`
  * [ ] Surface area and breaking changes have been addressed as agreed with the user (see “Clarify scope and surface area before implementing”).

  ### 📋 Standard Development Workflow

  1. Create a branch: `git checkout -b feature/your-feature-name`
  2. Create a changeset: `pnpm bump <patch|minor> --pkg <package> "<message>"` (required for any runtime behavior change to a published package — see [Creating Changelog Entries](#creating-changelog-entries-changesets))
  3. Run [Verification](#verification) before pushing
  4. Commit, then `gh pr create`

  The user may override this workflow (e.g., work directly on main).

  ### 📁 Git Worktrees for Parallel Feature Development

  Git worktrees allow you to work on multiple features simultaneously without switching branches in your main working directory. This is especially useful when you need to quickly switch context between different Linear tickets or have multiple features in progress.

  #### Creating a Worktree

  To spin off a new scope of work in a separate directory using git worktrees:

  ```bash
  git worktree add ../pull-instrument -b feat/pull-instrument
  ```

  **Important Conventions:**

  * The directory name and branch name should match (e.g., `pull-instrument` matches `feat/pull-instrument`)
  * Branch names should reference a Linear ticket when applicable (e.g., `feat/ENG-123-feature-name`)
  * Worktree directories are temporary and should be removed after the work is complete

  #### Working with Worktrees

  ```bash
  # Create a new worktree for a feature
  git worktree add ../my-feature -b feat/ENG-123-my-feature

  # Navigate to the worktree directory
  cd ../my-feature

  # Work on your feature normally
  # ... make changes, commit, push, create PR ...

  # List all worktrees
  git worktree list

  # Remove a worktree after PR is merged (run from main repo)
  git worktree remove ../my-feature

  # Remove the remote branch after cleanup
  git branch -d feat/ENG-123-my-feature
  git push origin --delete feat/ENG-123-my-feature

  # Prune stale worktree references
  git worktree prune
  ```

  #### When to Use Worktrees

  ✅ **Use worktrees when:**

  * Working on multiple features simultaneously
  * Need to quickly test/review another branch without stashing current work
  * Running long-running processes (tests, builds) while working on something else
  * Comparing implementations across different branches side-by-side

  ❌ **Use regular branches when:**

  * Working on a single feature at a time
  * Making quick hotfixes or small changes
  * The overhead of managing multiple directories isn't worth it

  **Reference**: [git-worktree documentation](https://git-scm.com/docs/git-worktree)

  ### When Working with Agents

  1. **Always call `graph.init()`** after creating agent relationships to persist to database
  2. **Use builder patterns** (`agent()`, `subAgent()`, `tool()`) instead of direct database manipulation
  3. **Preserve contextId** when implementing transfer/delegation logic - extract from task IDs if needed
  4. **Validate tool results** with proper type guards instead of unsafe casting
  5. **Test A2A communication end-to-end** when adding new agent relationships
  6. **Follow the mandatory requirements above** for all new features

  ### Performance Considerations

  * **Parallelize database operations** using `Promise.all()` instead of sequential `await` calls
  * **Optimize array processing** with `flatMap()` and `filter()` instead of nested loops
  * **Implement cleanup mechanisms** for debug files and logs to prevent memory leaks

  ### Internal Self-Calls: `getInProcessFetch()` vs `fetch`

  Any code in `agents-api` or `agents-work-apps` that makes **internal A2A calls or self-referencing API calls** (i.e. calling another route on the same service) **MUST** use `getInProcessFetch()` from `@inkeep/agents-core` instead of the global `fetch`.

  * `getInProcessFetch()` routes the request through the Hono app's middleware stack **in-process**, guaranteeing it stays on the same instance.
  * Global `fetch` sends the request over the network, where a load balancer may route it to a **different** instance — breaking features that depend on process-local state (e.g. the stream helper registry for SSE streaming).
  * This bug only manifests under load in multi-instance deployments and is extremely difficult to diagnose.

  **When to use:**

  | Scenario                                                     | Use                    |
  | ------------------------------------------------------------ | ---------------------- |
  | Internal A2A delegation/transfer (same service)              | `getInProcessFetch()`  |
  | Eval service calling the chat API on itself                  | `getInProcessFetch()`  |
  | Forwarding requests to internal workflow routes              | `getInProcessFetch()`  |
  | Slack/work-app calls to `/run/api/chat` or `/manage/` routes | `getInProcessFetch()`  |
  | Calling an **external** service or third-party API           | Global `fetch`         |
  | Test environments (falls back automatically)                 | Either (auto-fallback) |

  ### Route Authorization Pattern (`createProtectedRoute`)

  All API routes in `agents-api` **must** use `createProtectedRoute()` from `@inkeep/agents-core/middleware` instead of the plain `createRoute()` from `@hono/zod-openapi`. This is enforced by Biome lint rules and ensures every route has explicit authorization metadata (`x-authz`) in the OpenAPI spec.

  ```typescript
  import { createProtectedRoute, noAuth, inheritedAuth } from '@inkeep/agents-core/middleware';
  import { requireProjectPermission } from '../../middleware/projectAccess';

  // Standard protected route — pass the permission middleware directly
  app.openapi(
    createProtectedRoute({
      method: 'get',
      path: '/',
      permission: requireProjectPermission('view'),
      // ... rest of route config
    }),
    handler,
  );

  // Public route (no auth) — use noAuth()
  app.openapi(
    createProtectedRoute({
      method: 'get',
      path: '/callback',
      permission: noAuth(),
      security: [],
      // ... rest of route config
    }),
    handler,
  );

  // Route where auth is enforced by parent middleware — use inheritedAuth()
  app.openapi(
    createProtectedRoute({
      method: 'get',
      path: '/',
      permission: inheritedAuth({
        resource: 'organization',
        permission: 'member',
        description: 'Auth enforced by parent middleware in createApp.ts',
      }),
      // ... rest of route config
    }),
    handler,
  );
  ```

  **Key helpers:**

  | Helper                                       | When to use                                            |
  | -------------------------------------------- | ------------------------------------------------------ |
  | `requireProjectPermission('view' \| 'edit')` | Routes scoped to a project                             |
  | `requirePermission({ project: 'create' })`   | Org-level permission checks (admin)                    |
  | `noAuth()`                                   | Truly public endpoints (webhooks, OAuth callbacks)     |
  | `inheritedAuth(meta)`                        | Auth enforced by parent/global middleware              |
  | `inheritedRunApiKeyAuth()`                   | Run-domain routes behind API key middleware            |
  | `inheritedManageTenantAuth()`                | Manage-domain routes behind session/API key middleware |
  | `inheritedWorkAppsAuth()`                    | Work-apps routes behind OIDC/Slack middleware          |

  ### Common Gotchas

  * **Empty Task Messages**: Ensure task messages contain actual text content
  * **Context Extraction**: For delegation scenarios, extract contextId from task ID patterns like `task_math-demo-123456-chatcmpl-789`
  * **Tool Health**: MCP tools require health checks before use
  * **Agent Discovery**: Agents register capabilities via `/.well-known/{subAgentId}/agent.json` endpoints

  ### File Locations

  * **API Domains**: `agents-api/src/domains/` (unified API with manage/, run/, evals/ domains)
  * **Core Agents**: `agents-api/src/domains/run/agents/Agent.ts`, `agents-api/src/domains/run/agents/generateTaskHandler.ts`
  * **A2A Communication**: `agents-api/src/domains/run/a2a/`, `agents-api/src/domains/run/handlers/executionHandler.ts`
  * **Evaluations**: `agents-api/src/domains/evals/` (evaluation workflows, dataset runs, triggers)
  * **Management Routes**: `agents-api/src/domains/manage/routes/` (agent config, projects, tools, credentials)
  * **Database Layer**: `packages/agents-core/src/data-access/` (agents, tasks, conversations, tools)
  * **Builder Patterns**: `packages/agents-sdk/src/` (agent.ts, subAgent.ts, tool.ts, project.ts)
  * **Schemas**: `packages/agents-core/src/db/manage/manage-schema.ts`, `packages/agents-core/src/db/runtime/runtime-schema.ts` (Drizzle), `packages/agents-core/src/validation/` (Zod validation)
  * **Tests**: `agents-api/src/__tests__/` (unit and integration tests)
  * **UI Components**: `agents-manage-ui/src/components/` (React components)
  * **UI Pages**: `agents-manage-ui/src/app/` (Next.js pages and routing)
  * **Documentation**: `agents-docs/` (Next.js/Fumadocs public documentation site)
  * **Legacy Documentation**: `docs-legacy/` (internal/development notes)
  * **Examples**: `agents-cookbook/` for reference implementations

  ## Feature Development Examples

  ### Example: Adding a New Feature

  #### 1. Unit Test Example

  ```typescript
  // agents-api/src/__tests__/manage/newFeature.test.ts
  import { describe, it, expect, beforeEach } from 'vitest';
  import { NewFeature } from '../newFeature';

  describe('NewFeature', () => {
    let feature: NewFeature;
    
    beforeEach(() => {
      feature = new NewFeature({ tenantId: 'test-tenant' });
    });

    it('should initialize correctly', () => {
      expect(feature).toBeDefined();
      expect(feature.tenantId).toBe('test-tenant');
    });

    it('should handle errors gracefully', async () => {
      await expect(feature.process(null)).rejects.toThrow('Invalid input');
    });
  });
  ```

  #### 2. Agent Builder UI Component Example

  ```tsx
  // agents-manage-ui/src/components/new-feature/new-feature-form.tsx
  import { useState } from 'react';
  import { z } from 'zod';
  import { Button } from '../ui/button';
  import { Input } from '../ui/input';
  import { Form } from '../ui/form';

  const newFeatureSchema = z.object({
    name: z.string().min(1, 'Name is required'),
    config: z.object({
      enabled: z.boolean(),
      value: z.string().optional()
    })
  });

  export function NewFeatureForm() {
    const [formData, setFormData] = useState({});
    
    return (
      <Form schema={newFeatureSchema} onSubmit={handleSubmit}>
        {/* Form fields */}
      </Form>
    );
  }
  ```

  #### 3. Documentation Example

  ````mdx
  // agents-docs/content/docs/features/new-feature.mdx
  ---
  title: New Feature
  description: Brief description of what the feature does
  ---

  ## Overview
  Brief description of what the feature does and why it's useful.

  ## Usage
  ```typescript
  const feature = new NewFeature({
    id: 'feature-1',
    config: { enabled: true }
  });

  await feature.execute();
  ````

  ## API Reference

  * `NewFeature(config)` - Creates a new feature instance
  * `execute()` - Executes the feature
  * `validate()` - Validates configuration

  ## Examples

  \[Include practical examples here]

  ````

  // Also update agents-docs/source.config.ts to include the new page in navigation

  ## Debugging Commands

  ### Jaeger / OTLP Tracing Debugging
  Replace `service` with the current service name (e.g., `inkeep-agents-api` in prod, `inkeep-agents-api-test` in tests). If using SigNoz/OTLP, point to that host/port instead of localhost.

  ```bash
  # Get all services
  curl "http://localhost:16686/api/services"

  # Get operations for a service
  curl "http://localhost:16686/api/operations?service=inkeep-agents-api"

  # Search traces for recent activity (last hour)
  curl "http://localhost:16686/api/traces?service=inkeep-agents-api&limit=20&lookback=1h"

  # Search traces by operation name
  curl "http://localhost:16686/api/traces?service=inkeep-agents-api&operation=agent.generate&limit=10"

  # Search traces by tags (useful for finding specific agent/conversation)
  curl "http://localhost:16686/api/traces?service=inkeep-agents-api&tags=%7B%22agent.id%22:%22qa-agent%22%7D&limit=10"

  # Search traces by tags for conversation ID
  curl "http://localhost:16686/api/traces?service=inkeep-agents-api&tags=%7B%22conversation.id%22:%22conv-123%22%7D"

  # Get specific trace by ID
  curl "http://localhost:16686/api/traces/{trace-id}"

  # Search for traces with errors
  curl "http://localhost:16686/api/traces?service=inkeep-agents-api&tags=%7B%22error%22:%22true%22%7D&limit=10"

  # Search for tool call traces
  curl "http://localhost:16686/api/traces?service=inkeep-agents-api&operation=tool.call&limit=10"

  # Search traces within time range (Unix timestamps)
  curl "http://localhost:16686/api/traces?service=inkeep-agents-api&start=1640995200000000&end=1641081600000000"
  ````

  ### Common Debugging Workflows

  **Debugging Agent Transfers:**

  1. View traces: `curl "http://localhost:16686/api/traces?service=inkeep-agents-api&tags=%7B%22conversation.id%22:%22conv-123%22%7D"`

  **Debugging Tool Calls:**

  1. Find tool call traces: `curl "http://localhost:16686/api/traces?service=inkeep-agents-api&operation=tool.call&limit=10"`

  **Debugging Task Delegation:**

  1. Trace execution flow: `curl "http://localhost:16686/api/traces?service=inkeep-agents-api&tags=%7B%22task.id%22:%22task-id%22%7D"`

  **Debugging Performance Issues:**

  1. Find slow operations: `curl "http://localhost:16686/api/traces?service=inkeep-agents-api&minDuration=5s"`
  2. View error traces: `curl "http://localhost:16686/api/traces?service=inkeep-agents-api&tags=%7B%22error%22:%22true%22%7D"`

  ## AI Coding Assistant Guidance

  This repository keeps one canonical source for agent guidance and skills, mirrored into each tool's native location.

  | Source (edit this) | Generated mirror (don't edit) |
  | ------------------ | ----------------------------- |
  | `AGENTS.md`        | `CLAUDE.md`                   |
  | `.agents/skills/`  | `.codex/skills/`              |

  Cursor reads `.agents/skills/` natively and Claude Code loads skills via plugins, so neither needs a generated mirror. AGENTS.md is always loaded by the Agent Harness, and Skills provide on-demand expertise for specific development tasks. They are auto-discovered — no need to document them here.

  ## Cursor Cloud specific instructions

  Non-obvious caveats for Cursor Cloud agents bringing this subtree up in an ephemeral Linux VM (standard commands are documented above):

  * Node: `nvm use default` (22.22.2) before pnpm; the VM base `/exec-daemon/node` (22.14.0) fails `engine-strict` (`agents-core` needs `>=22.18.0`). Login shells select it via `~/.bashrc`.
  * Docker (Doltgres `:5432`, Postgres `:5433`, SpiceDB `:50051`, Mailpit) is preinstalled but not auto-started: `sudo dockerd &` then `sudo chmod 666 /var/run/docker.sock`. `/etc/docker/daemon.json` is preset for `fuse-overlayfs` (containerd snapshotter disabled, required on this kernel).
  * Build `@inkeep/agents-core` before `pnpm setup-dev` (setup imports its `dist/setup`). Then `pnpm dev` serves API `:3002` (`/health` 204) and Manage UI `:3000` (local-dev auto-login `admin@example.com`).
  * `.env` needs `ANTHROPIC_API_KEY`; a placeholder unblocks DB/API/UI + project CRUD, real agent chat needs a valid key. `setup-dev`'s sample-project push may fail (`inkeep` bin not resolvable) but is non-fatal.
</>

Human-only workflows (process, PR etiquette, roadmap context) belong in this guide and the broader contributing docs above.

## Questions?

If you have questions about contributing, please:

1. Check existing issues and discussions
2. Open a new issue if your question isn't addressed
3. Reach out to the maintainers

Thank you for contributing!


# Data Layer constraints on Project relationships (/community/contributing/project-constraints)

How the Inkeep Agent Framework ensures data integrity with project constraints



## Project Constraints and Validation

The Inkeep Agent Framework implements multiple layers of validation to ensure that all agents and resources are associated with valid projects.

## Database-Level Constraints

### Foreign Key Constraints

All tables that reference projects include foreign key constraints to ensure referential integrity:

```sql
FOREIGN KEY (tenant_id, project_id)
REFERENCES projects(tenant_id, id)
ON DELETE CASCADE
```

This ensures that:

* No data can be inserted for non-existent projects
* Deleting a project cascades to remove all associated resources
* Data integrity is maintained at the database level

### Tables with Project Constraints

The following tables have foreign key constraints to the `projects` table:

1. `agent` - Agent definitions (top-level Agents)
2. `sub_agents` - Sub Agent configurations
3. `sub_agent_relations` - Relationships between Sub Agents
4. `tools` - Tool configurations
5. `context_configs` - Context configurations
6. `external_agents` - External agent references
7. And more...

## Runtime Validation

### CLI Validation

The Inkeep CLI validates project existence before pushing agents:

```typescript
// CLI checks if project exists
const existingProject = await getProject(dbClient)({
  scopes: { tenantId, projectId },
});

if (!existingProject) {
  // Prompt user to create project
  // ...
}
```

### Data Access Layer Validation

The core package provides validation utilities for runtime checks:

```typescript
import { validateProjectExists } from "@inkeep/agents-core";

// Validate before any operation
await validateProjectExists(db, tenantId, projectId);
```

### Wrapped Operations

Data access functions can be wrapped with automatic validation:

```typescript
import { withProjectValidation } from "@inkeep/agents-core";

const validatedCreateAgent = withProjectValidation(db, createAgent);
// Now automatically validates project before creating agent
```

## Implementation Details

### Schema Definition

```typescript
export const agents = sqliteTable(
  "agent",
  {
    tenantId: text("tenant_id").notNull(),
    projectId: text("project_id").notNull(),
    // ... other columns
  },
  (table) => [
    primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
    foreignKey({
      columns: [table.tenantId, table.projectId],
      foreignColumns: [projects.tenantId, projects.id],
      name: "agent_project_fk",
    }),
  ]
);
```

### Enabling Foreign Keys in SQLite

SQLite requires explicit enabling of foreign key constraints:

```sql
PRAGMA foreign_keys = ON;
```

This should be set when creating the database connection:

```typescript
const client = createClient({
  url: dbUrl,
  authToken: authToken,
});

// Enable foreign keys
await client.execute("PRAGMA foreign_keys = ON");
```

## Error Handling

When a constraint violation occurs, appropriate error messages guide users:

### CLI Error

```
⚠ Project "my-project" does not exist
? Would you like to create it? (Y/n)
```

### Database Error

```
Error: Project with ID "my-project" does not exist for tenant "my-tenant".
Please create the project first before adding resources to it.
```

## Best Practices

1. **Always validate at multiple levels**: Database constraints, runtime validation, and UI validation
2. **Provide clear error messages**: Help users understand what went wrong and how to fix it
3. **Offer solutions**: When a project doesn't exist, offer to create it
4. **Use transactions**: Ensure atomicity when creating projects and related resources
5. **Test constraints**: Verify that constraints work as expected in tests

## Migration Guide

For existing databases without constraints:

1. **Backup your database** before applying migrations
2. **Check for orphaned data**: Find resources referencing non-existent projects
3. **Clean up orphaned data** or create missing projects
4. **Apply foreign key constraints** using migrations
5. **Enable foreign key enforcement** in your database connection

```sql
-- Find orphaned agents
SELECT a.* FROM agent a
LEFT JOIN projects p
  ON a.tenant_id = p.tenant_id
  AND a.project_id = p.id
WHERE p.id IS NULL;
```

## Benefits

* **Data Integrity**: Prevents orphaned data and maintains consistency
* **Clear User Experience**: Users are guided to create projects when needed
* **Easier Debugging**: Constraint violations are caught early
* **Simplified Cleanup**: Cascading deletes remove all related data
* **Better Documentation**: Constraints document relationships in the schema


# Spans and Traces (/community/contributing/spans)

OpenTelemetry spans for distributed tracing and observability in the Inkeep Agent Framework



## Overview

The Inkeep Agent Framework uses OpenTelemetry for distributed tracing and observability. Spans provide detailed visibility into the execution flow of agents, context resolution, tool execution, and other framework operations.

## Getting Started with Spans

### 1. Import Required Dependencies

```typescript
import { SpanStatusCode, type Span } from "@opentelemetry/api";
import { getTracer, setSpanWithError } from "../tracer";
```

### 2. Get the Tracer

```typescript
// Use the centralized tracer utility
const tracer = getTracer("your-service-name");
```

## Creating and Using Spans

```typescript
return tracer.startActiveSpan(
  "context.resolve",
  {
    attributes: {
      "context.config_id": contextConfig.id,
      "context.trigger_event": options.triggerEvent,
    },
  },
  async (span: Span) => {
    try {
      // Your operation logic here
      return result;
    } catch (error) {
      // Use setSpanWithError for consistent error handling
      setSpanWithError(span, error);
      throw error;
    }
  }
);
```

## Setting Span Attributes

### Basic Attributes

```typescript
span.setAttributes({
  "user.id": userId,
  "request.method": "POST",
});
```

## Adding Events to Spans

### Recording Important Milestones

```typescript
// Add events for significant operations
span.addEvent("context.fetch_started", {
  definitionId: definition.id,
  url: definition.fetchConfig.url,
});
```

### Error Events

```typescript
span.addEvent("error.validation_failed", {
  definitionId: definition.id,
  error_type: "json_schema_validation",
  error_details: errorMessage,
});
```

## Error Handling and Status

### Using setSpanWithError Utility

The framework provides a convenient `setSpanWithError` utility function that handles error recording and status setting:

```typescript
try {
  // Your operation
} catch (error) {
  // Use the setSpanWithError utility for consistent error handling
  setSpanWithError(span, error);
  throw error;
}
```

## Cache Telemetry Attributes

LLM generation spans carry cache telemetry emitted by the gateway cost middleware. These attributes are defined in `packages/agents-core/src/constants/otel-attributes.ts` and consumed by the Manage UI trace timeline and cost dashboard.

### Outcome attributes (OTel-GenAI semconv v1.41.1 aligned)

| Attribute                                  | Type   | Description                                        |
| ------------------------------------------ | ------ | -------------------------------------------------- |
| `gen_ai.usage.cache_read.input_tokens`     | number | Tokens served from the provider cache on this call |
| `gen_ai.usage.cache_creation.input_tokens` | number | Tokens written to the provider cache on this call  |

`gen_ai.usage.input_tokens` (the existing total) is cache-inclusive and unchanged. `no_cache_input_tokens` is derivable as `input_tokens − cache_read − cache_creation` and is not emitted.

### Intent attributes (Inkeep `cache.intent.*` namespace)

| Attribute                       | Type      | Description                                                                                                                                                                                     |
| ------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cache.intent.marker_count`     | int (0–4) | Number of cache markers attached to the request prefix                                                                                                                                          |
| `cache.intent.prefix_signature` | string    | 10-character SHA-256 hex prefix of the cacheable prefix (system prompts and tool definitions), used to distinguish MISS-expected (prefix changed) from MISS-regression (stable prefix, no read) |

### Cache state (consumer-derived, not a span attribute)

`cache_state` is **not emitted as a span attribute** — it is computed by consumers (Manage UI, the `pnpm cache-debug` CLI) from the four attributes above plus prior-turn signature comparison:

| State                       | Derivation                                                                        |
| --------------------------- | --------------------------------------------------------------------------------- |
| `HIT`                       | `marker_count > 0` and `cache_read > 0`                                           |
| `MISS-expected`             | `marker_count > 0`, `cache_read = 0`, prefix signature differs from previous call |
| `MISS-regression`           | `marker_count > 0`, `cache_read = 0`, prefix signature identical to previous call |
| `NOT-ATTEMPTED`             | `marker_count = 0`                                                                |
| `NOT-SUPPORTED-BY-PROVIDER` | Provider metadata absent or provider does not support marker-based caching        |

When adding new span attributes, follow the same append-only rule: additions to `USAGE_COST_AGGREGATION_ORDER` in `signoz-stats.ts` must be appended, never reordered, because the consumer side uses positional indexing.

## Best Practices

### 1. Consistent Naming Convention

The span naming convention follows a hierarchical structure that mirrors your code organization.

```typescript
// Format: 'class.function'
// Use descriptive span names that follow a hierarchical structure

// Agent operations
"agent.generate";
"agent.tool_execute";
"agent.transfer";

// Context operations
"context.resolve";
"context.fetch";
```

#### Naming Rules

1. **Class First**: Start with the class/module name (e.g., `agent`, `context`, `tool`)
2. **Function Second**: Follow with the specific function/method (e.g., `generate`, `resolve`)
3. **Use Underscores**: For multi-word functions, use underscores (e.g., `tool_execute`, `cache_lookup`)
4. **Consistent Casing**: Use lowercase with underscores for consistency

## Configuration and Setup

### Environment Variables

```bash
# OpenTelemetry configuration
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4317
```

### Instrumentation Setup

The framework automatically sets up OpenTelemetry instrumentation in `src/instrumentation.ts`:

## Examples in the Codebase

### Agent Operations

See `src/agents/Agent.ts` for span usage in agent generation and tool execution.

**Example from Agent.ts:**

```typescript
// Class: Agent
// Function: generate
return tracer.startActiveSpan(
  "agent.generate",
  {
    attributes: {
      "agent.id": this.id,
      "agent.name": this.name,
    },
  },
  async (span: Span) => {
    // ... implementation
  }
);
```

## Summary

Spans provide powerful observability into your Inkeep Agent Framework operations. By following these patterns:

1. **Use `getTracer()`** for consistent tracing
2. **Use consistent naming**
3. **Set meaningful attributes** for searchability
4. **Handle errors properly** using `setSpanWithError` for consistent error handling
5. **Use `startActiveSpan`** for automatic lifecycle management

This will give you comprehensive visibility into your agent operations, making debugging and performance optimization much easier.


# A2A (/api-reference/a2a)

Agent-to-Agent communication endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/run/agents/.well-known/agent.json","method":"get"}]} showTitle={true} />


# Agents (/api-reference/agents)

Operations for managing agents



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/available-agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"put"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/full","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}/component/{artifactComponentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}/component/{artifactComponentId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/component/{artifactComponentId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}/component/{dataComponentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}/component/{dataComponentId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/component/{dataComponentId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/related","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/tool-status","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"delete"}]} showTitle={true} />


# API Keys (/api-reference/api-keys)

Operations for managing API keys



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/playground/token","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/api-keys","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/api-keys","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/api-keys/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/api-keys/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/api-keys/{id}","method":"delete"}]} showTitle={true} />


# Apps (/api-reference/apps)

Operations for managing app credentials



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/apps","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/apps","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/apps","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/apps/{appId}/auth/keys","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/apps/{appId}/auth/keys","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/apps/{appId}/auth/keys/{kid}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/apps/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/apps/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/apps/{id}","method":"delete"}]} showTitle={true} />


# Artifact Components (/api-reference/artifact-components)

Operations for managing artifact components



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}/component/{artifactComponentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}/component/{artifactComponentId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/component/{artifactComponentId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/artifact-components","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/artifact-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/artifact-components/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/artifact-components/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/artifact-components/{id}","method":"delete"}]} showTitle={true} />


# Auth (/api-reference/auth)

Authentication endpoints for app credentials



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/run/auth/apps/{appId}/anonymous-session","method":"post"},{"path":"/run/auth/challenge","method":"get"},{"path":"/run/auth/challenge","method":"post"},{"path":"/run/auth/challenge/verify","method":"post"},{"path":"/run/auth/pow/challenge","method":"get"}]} showTitle={true} />


# Branches (/api-reference/branches)

Operations for managing branches



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/branches","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/branches","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/branches/agents/{agentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/branches/merge","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/branches/merge/preview","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/branches/{branchName}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/branches/{branchName}","method":"delete"}]} showTitle={true} />


# Channels (/api-reference/channels)

Operations for managing Slack channels



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/work-apps/slack/workspaces/{teamId}/channels","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/bulk","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/bulk","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"put"}]} showTitle={true} />


# Chat (/api-reference/chat)

Chat completions endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/run/api/chat","method":"post"},{"path":"/run/v1/chat/completions","method":"post"}]} showTitle={true} />


# CLI (/api-reference/cli)

CLI authentication endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/api/cli/me","method":"get"}]} showTitle={true} />


# Context Configs (/api-reference/context-configs)

Operations for managing context configurations



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/context-configs","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/context-configs","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/context-configs/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/context-configs/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/context-configs/{id}","method":"delete"}]} showTitle={true} />


# Conversations (/api-reference/conversations)

Operations for managing conversations



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/conversations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/conversations/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/conversations/{id}/bounds","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/conversations/{id}/media/{mediaKey}","method":"get"},{"path":"/run/v1/conversations","method":"get"},{"path":"/run/v1/conversations/{conversationId}","method":"get"},{"path":"/run/v1/conversations/{conversationId}/pending-approvals","method":"get"},{"path":"/run/v1/conversations/{conversationId}/stream","method":"get"}]} showTitle={true} />


# Credential Gateway (/api-reference/credential-gateway)

RFC 8693 token exchange and platform discovery endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/credential-gateway/.well-known/platforms","method":"get"},{"path":"/credential-gateway/token","method":"post"}]} showTitle={true} />


# Credential Stores (/api-reference/credential-stores)

Operations for managing credential stores



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/credential-stores","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/credential-stores/{id}/credentials","method":"post"}]} showTitle={true} />


# Credentials (/api-reference/credentials)

Operations for managing credentials



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/credentials","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/credentials","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/credentials/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/credentials/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/credentials/{id}","method":"delete"}]} showTitle={true} />


# Data Components (/api-reference/data-components)

Operations for managing data components



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}/component/{dataComponentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}/component/{dataComponentId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/component/{dataComponentId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/data-components","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/data-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/data-components/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/data-components/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/data-components/{id}","method":"delete"}]} showTitle={true} />


# Entitlements (/api-reference/entitlements)

Operations for managing organization entitlements



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/entitlements","method":"get"}]} showTitle={true} />


# Evaluations (/api-reference/evaluations)

Operations for managing evaluations



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/evals/tenants/{tenantId}/projects/{projectId}/evaluate-conversation","method":"post"},{"path":"/evals/tenants/{tenantId}/projects/{projectId}/evaluate-conversations","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-items/{datasetId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-items/{datasetId}/items","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-items/{datasetId}/items/bulk","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-items/{datasetId}/items/{itemId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-items/{datasetId}/items/{itemId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-items/{datasetId}/items/{itemId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs/by-dataset/{datasetId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs/{runConfigId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs/{runConfigId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs/{runConfigId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs/{runConfigId}/run","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs/{runConfigId}/schedule","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs/{runConfigId}/schedule","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-run-configs/{runConfigId}/schedule","method":"put"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-runs/by-conversation/{conversationId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-runs/by-dataset/{datasetId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-runs/{runId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-runs/{runId}/items","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-runs/{runId}/items/{itemId}/rerun","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/dataset-runs/{runId}/rerun","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/datasets","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/datasets","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/datasets/{datasetId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/datasets/{datasetId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/datasets/{datasetId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/datasets/{datasetId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/datasets/{datasetId}/agents/{agentId}","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/datasets/{datasetId}/agents/{agentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-job-configs","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-job-configs","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-job-configs/{configId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-job-configs/{configId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-job-configs/{configId}/evaluators","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-job-configs/{configId}/evaluators/{evaluatorId}","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-job-configs/{configId}/evaluators/{evaluatorId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-job-configs/{configId}/results","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-results","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-results/{resultId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-results/{resultId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-results/{resultId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-run-configs","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-run-configs","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-run-configs/{configId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-run-configs/{configId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-run-configs/{configId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-run-configs/{configId}/results","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-suite-configs","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-suite-configs","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-suite-configs/{configId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-suite-configs/{configId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-suite-configs/{configId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-suite-configs/{configId}/evaluators","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-suite-configs/{configId}/evaluators/{evaluatorId}","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluation-suite-configs/{configId}/evaluators/{evaluatorId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators/batch","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators/batch-agent-scopes","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators/{evaluatorId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators/{evaluatorId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators/{evaluatorId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators/{evaluatorId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators/{evaluatorId}/agents/{agentId}","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/evals/evaluators/{evaluatorId}/agents/{agentId}","method":"delete"}]} showTitle={true} />


# Events (/api-reference/events)

Application-level event ingest and webhook delivery



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/run/v1/events","method":"post"}]} showTitle={true} />


# Executions (/api-reference/executions)

Durable agent execution endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/run/api/executions","method":"post"},{"path":"/run/api/executions/{executionId}","method":"get"},{"path":"/run/api/executions/{executionId}/approvals/{toolCallId}","method":"post"},{"path":"/run/api/executions/{executionId}/stream","method":"get"}]} showTitle={true} />


# External Agents (/api-reference/external-agents)

Operations for managing external agents



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/external-agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/external-agents","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/external-agents/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/external-agents/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/external-agents/{id}","method":"delete"}]} showTitle={true} />


# Feedback (/api-reference/feedback)

Operations for managing feedback



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/feedback","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/feedback","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/feedback/bulk","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/feedback/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/feedback/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/feedback/{id}","method":"delete"},{"path":"/run/v1/feedback","method":"post"}]} showTitle={true} />


# Function Tools (/api-reference/function-tools)

Operations for managing function tools



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/function-tools","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/function-tools","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/function-tools/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/function-tools/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/function-tools/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools/function-tool/{functionToolId}/sub-agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools/sub-agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools/sub-agent/{subAgentId}/function-tool/{functionToolId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools/sub-agent/{subAgentId}/function-tool/{functionToolId}/exists","method":"get"}]} showTitle={true} />


# Functions (/api-reference/functions)

Operations for managing functions



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/functions","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/functions","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/functions/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/functions/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/functions/{id}","method":"delete"}]} showTitle={true} />


# GitHub (/api-reference/github)

GitHub App integration endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/github/install-url","method":"get"},{"path":"/manage/tenants/{tenantId}/github/installations","method":"get"},{"path":"/manage/tenants/{tenantId}/github/installations/{installationId}","method":"get"},{"path":"/manage/tenants/{tenantId}/github/installations/{installationId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/github/installations/{installationId}/disconnect","method":"post"},{"path":"/manage/tenants/{tenantId}/github/installations/{installationId}/reconnect","method":"post"},{"path":"/manage/tenants/{tenantId}/github/installations/{installationId}/sync","method":"post"}]} showTitle={true} />


# Improvements (/api-reference/improvements)

Operations for agent improvement runs (copilot-assisted edits)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/copilot-runs","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/trigger","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/{branchName}/continue","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/{branchName}/conversation","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/{branchName}/diff","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/{branchName}/eval-summary","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/{branchName}/merge","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/{branchName}/reject","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/improvements/{branchName}/revert","method":"post"}]} showTitle={true} />


# Invitations (/api-reference/invitations)

Operations for managing organization invitations



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/api/invitations","method":"post"}]} showTitle={true} />


# MCP Catalog (/api-reference/mcp-catalog)

Operations for MCP catalog



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/mcp-catalog","method":"get"}]} showTitle={true} />


# MCP (/api-reference/mcp)

MCP (Model Context Protocol) endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/run/v1/mcp","method":"post"}]} showTitle={true} />


# OAuth (/api-reference/oauth)

OAuth authentication endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/oauth/callback","method":"get"},{"path":"/work-apps/slack/install","method":"get"},{"path":"/work-apps/slack/oauth_redirect","method":"get"}]} showTitle={true} />


# Project Members (/api-reference/project-members)

Operations for managing project members



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/members","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/members","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/members/{userId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/members/{userId}","method":"delete"}]} showTitle={true} />


# Project Permissions (/api-reference/project-permissions)

Operations for managing project permissions



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/permissions","method":"get"}]} showTitle={true} />


# Projects (/api-reference/projects)

Operations for managing projects



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/project-full","method":"post"},{"path":"/manage/tenants/{tenantId}/project-full/{projectId}","method":"get"},{"path":"/manage/tenants/{tenantId}/project-full/{projectId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/project-full/{projectId}","method":"put"},{"path":"/manage/tenants/{tenantId}/project-full/{projectId}/with-relation-ids","method":"get"},{"path":"/manage/tenants/{tenantId}/projects","method":"get"},{"path":"/manage/tenants/{tenantId}/projects","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/github-access","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/github-access","method":"put"}]} showTitle={true} />


# Refs (/api-reference/refs)

Operations for the resolved ref (branch name, tag name, or commit hash)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/refs/resolve","method":"get"}]} showTitle={true} />


# Scheduled Triggers (/api-reference/scheduled-triggers)

Operations for managing scheduled triggers



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/upcoming-runs","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/invocations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/invocations/{invocationId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/invocations/{invocationId}/cancel","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/invocations/{invocationId}/rerun","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/run","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/users","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/users","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/users","method":"put"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/scheduled-triggers/{id}/users/{userId}","method":"delete"}]} showTitle={true} />


# Skills (/api-reference/skills)

Reusable instruction blocks that can be attached to multiple sub-agents and ordered for priority



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-skills","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-skills/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-skills/agent/{subAgentId}/skill/{skillId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills/{id}/files","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills/{id}/files/{fileId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills/{id}/files/{fileId}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/skills/{id}/files/{fileId}","method":"delete"}]} showTitle={true} />


# Slack (/api-reference/slack)

Slack App integration endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/work-apps/slack/install","method":"get"},{"path":"/work-apps/slack/oauth_redirect","method":"get"},{"path":"/work-apps/slack/users/connect","method":"post"},{"path":"/work-apps/slack/users/disconnect","method":"post"},{"path":"/work-apps/slack/users/link-status","method":"get"},{"path":"/work-apps/slack/users/link/verify-token","method":"post"},{"path":"/work-apps/slack/users/status","method":"get"},{"path":"/work-apps/slack/workspaces","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/channels","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/bulk","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/bulk","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/health","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/join-from-workspace","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/join-from-workspace","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/settings","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/settings","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/test-message","method":"post"},{"path":"/work-apps/slack/workspaces/{teamId}/users","method":"get"}]} showTitle={true} />


# SubAgents (/api-reference/subagents)

Operations for managing sub agents



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools/function-tool/{functionToolId}/sub-agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools/sub-agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools/sub-agent/{subAgentId}/function-tool/{functionToolId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-function-tools/sub-agent/{subAgentId}/function-tool/{functionToolId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-relations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-relations","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-relations/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-relations/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-relations/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations/tool/{toolId}/sub-agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/external-agent-relations/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/team-agent-relations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/team-agent-relations","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/team-agent-relations/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/team-agent-relations/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/team-agent-relations/{id}","method":"delete"}]} showTitle={true} />


# Third-Party MCP Servers (/api-reference/third-party-mcp-servers)

Operations for managing third-party MCP servers



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/third-party-mcp-servers","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/third-party-mcp-servers/oauth-redirect","method":"post"}]} showTitle={true} />


# Tools (/api-reference/tools)

Operations for managing MCP tools



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations/tool/{toolId}/sub-agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-tool-relations/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/tool-status","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{id}/oauth/login","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{id}/user-credential","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{toolId}/github-access","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{toolId}/github-access","method":"put"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{toolId}/slack-access","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/tools/{toolId}/slack-access","method":"put"}]} showTitle={true} />


# Triggers (/api-reference/triggers)

Operations for managing triggers



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}/invocations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}/invocations/{invocationId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}/rerun","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}/users","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}/users","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}/users","method":"put"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{id}/users/{userId}","method":"delete"}]} showTitle={true} />


# User Profile (/api-reference/user-profile)

Operations for managing user profile



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/api/users/{userId}/profile","method":"get"},{"path":"/manage/api/users/{userId}/profile","method":"patch"}]} showTitle={true} />


# User Project Memberships (/api-reference/user-project-memberships)

Operations for managing user project memberships



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/users/{userId}/project-memberships","method":"get"}]} showTitle={true} />


# Users (/api-reference/users)

Operations for managing users



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/work-apps/slack/users/connect","method":"post"},{"path":"/work-apps/slack/users/disconnect","method":"post"},{"path":"/work-apps/slack/users/link-status","method":"get"},{"path":"/work-apps/slack/users/link/verify-token","method":"post"},{"path":"/work-apps/slack/users/status","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/users","method":"get"}]} showTitle={true} />


# Webhooks (/api-reference/webhooks)

Webhook endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/webhook-destinations","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/webhook-destinations","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/webhook-destinations/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/webhook-destinations/{id}","method":"patch"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/webhook-destinations/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/webhook-destinations/{id}/test","method":"post"},{"path":"/run/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{triggerId}","method":"post"}]} showTitle={true} />


# Work Apps (/api-reference/work-apps)

Work app integrations (Slack, Teams, etc.)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/work-apps/slack/install","method":"get"},{"path":"/work-apps/slack/oauth_redirect","method":"get"},{"path":"/work-apps/slack/users/connect","method":"post"},{"path":"/work-apps/slack/users/disconnect","method":"post"},{"path":"/work-apps/slack/users/link-status","method":"get"},{"path":"/work-apps/slack/users/link/verify-token","method":"post"},{"path":"/work-apps/slack/users/status","method":"get"},{"path":"/work-apps/slack/workspaces","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/channels","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/bulk","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/bulk","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/channels/{channelId}/settings","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/health","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/join-from-workspace","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/join-from-workspace","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/settings","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/settings","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/test-message","method":"post"},{"path":"/work-apps/slack/workspaces/{teamId}/users","method":"get"}]} showTitle={true} />


# Workflows (/api-reference/workflows)

Workflow trigger endpoints



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/api/cron/cleanup-stream-chunks","method":"get"},{"path":"/api/deploy/restart-scheduler","method":"post"},{"path":"/evals/tenants/{tenantId}/projects/{projectId}/evaluate-conversations-by-job","method":"post"}]} showTitle={true} />


# Workspaces (/api-reference/workspaces)

Operations for managing Slack workspaces



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/work-apps/slack/workspaces","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}","method":"delete"},{"path":"/work-apps/slack/workspaces/{teamId}/health","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/join-from-workspace","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/join-from-workspace","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/settings","method":"get"},{"path":"/work-apps/slack/workspaces/{teamId}/settings","method":"put"},{"path":"/work-apps/slack/workspaces/{teamId}/test-message","method":"post"}]} showTitle={true} />


# Configure Authentication (/deployment/authentication)

Set up authentication and authorization for user sign-in and team management



Configure user authentication, admin credentials, and optional OAuth providers.

<Note>
  For a feature overview of authentication and authorization, see [Access Control](/visual-builder/access-control).
</Note>

## Architecture

The framework uses two components for access control:

| Component                                   | Purpose                                            |
| ------------------------------------------- | -------------------------------------------------- |
| [Better Auth](https://www.better-auth.com/) | User authentication, sessions, and OAuth providers |
| [SpiceDB](https://authzed.com/spicedb)      | Fine-grained authorization and permission checks   |

Better Auth handles user sign-in and supports many authentication plugins including GitHub, Microsoft, SAML, passkeys, and more. See the [Better Auth documentation](https://www.better-auth.com/docs/authentication/email-password) to add additional sign-in methods.

SpiceDB manages organization and project-level permissions using a relationship-based access control model.

## Prerequisites

* Docker Compose environment running (see [Local Development](/deployment/docker-local))
* At least one AI provider API key configured

## Environment Variables Reference

### Authentication

| Variable                           | Required | Description                               |
| ---------------------------------- | -------- | ----------------------------------------- |
| `BETTER_AUTH_SECRET`               | Yes      | Secret for session encryption (32+ chars) |
| `INKEEP_AGENTS_MANAGE_UI_USERNAME` | Yes      | Initial admin email address               |
| `INKEEP_AGENTS_MANAGE_UI_PASSWORD` | Yes      | Initial admin password (8+ chars)         |

### Authorization

| Variable                | Required | Description                                        |
| ----------------------- | -------- | -------------------------------------------------- |
| `SPICEDB_ENDPOINT`      | Yes      | SpiceDB gRPC endpoint (default: `localhost:50051`) |
| `SPICEDB_PRESHARED_KEY` | Yes      | SpiceDB preshared key for authentication           |

### OAuth Providers (Optional)

| Variable                     | Required | Description                                |
| ---------------------------- | -------- | ------------------------------------------ |
| `PUBLIC_GOOGLE_CLIENT_ID`    | No       | Google OAuth client ID                     |
| `GOOGLE_CLIENT_SECRET`       | No       | Google OAuth client secret                 |
| `PUBLIC_MICROSOFT_CLIENT_ID` | No       | Microsoft Entra ID application (client) ID |
| `MICROSOFT_CLIENT_SECRET`    | No       | Microsoft Entra ID client secret           |

## Configuring Authentication

Authentication is enabled by default. Configure the required environment variables to set up your admin credentials and session security.

<Steps>
  <Step>
    ### Generate a secret

    Create a secure secret for session encryption:

    ```bash
    openssl rand -base64 32
    ```
  </Step>

  <Step>
    ### Configure environment variables

    Add these to your `.env` file:

    ```dotenv title=".env"
    # Authentication secret (paste your generated secret)
    BETTER_AUTH_SECRET=<your-generated-secret>

    # Initial admin credentials
    INKEEP_AGENTS_MANAGE_UI_USERNAME=admin@example.com
    INKEEP_AGENTS_MANAGE_UI_PASSWORD=<secure-password-8-chars-min>

    # Authorization (SpiceDB)
    SPICEDB_ENDPOINT=localhost:50051
    SPICEDB_PRESHARED_KEY=dev-secret-key
    ```
  </Step>

  <Step>
    ### Restart services

    ```bash
    docker compose up -d
    ```
  </Step>

  <Step>
    ### Sign in

    Open [http://localhost:3000](http://localhost:3000). When using `pnpm dev`, you'll be signed in automatically using the credentials configured above. For Docker deployments, sign in manually with your admin credentials.
  </Step>
</Steps>

## Adding OAuth Providers

### Google OAuth

<Steps>
  <Step>
    ### Create OAuth application

    1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
    2. Navigate to **APIs & Services** → **Credentials**
    3. Click **Create Credentials** → **OAuth client ID**
    4. Select **Web application**
  </Step>

  <Step>
    ### Configure redirect URI

    Add this authorized redirect URI:

    ```
    {your-app-url}/api/auth/callback/google
    ```

    For local development: `http://localhost:3002/api/auth/callback/google`
  </Step>

  <Step>
    ### Add credentials to environment

    ```dotenv title=".env"
    PUBLIC_GOOGLE_CLIENT_ID=<your-client-id>
    GOOGLE_CLIENT_SECRET=<your-client-secret>
    ```
  </Step>

  <Step>
    ### Restart services

    ```bash
    docker compose up -d
    ```

    The Google sign-in option will now appear on the login page.
  </Step>
</Steps>

### Microsoft Entra ID

<Steps>
  <Step>
    ### Register an application

    1. Go to the [Azure Portal](https://portal.azure.com/)
    2. Navigate to **Microsoft Entra ID** → **App registrations** → **New registration**
    3. Give the app a name (e.g. *Inkeep Agents*)
    4. Under **Supported account types**, choose the audience you want to allow:
       * *Accounts in this organizational directory only* — restricts sign-in to your Entra tenant
       * *Accounts in any organizational directory* — any Entra tenant
       * *Accounts in any organizational directory and personal Microsoft accounts* — matches the default (`common`) tenant behavior used by the framework
  </Step>

  <Step>
    ### Configure redirect URI

    Under **Authentication** → **Platform configurations** → **Add a platform** → **Web**, add this redirect URI:

    ```
    {your-app-url}/api/auth/callback/microsoft
    ```

    For local development: `http://localhost:3002/api/auth/callback/microsoft`
  </Step>

  <Step>
    ### Create a client secret

    Under **Certificates & secrets** → **Client secrets** → **New client secret**, create a secret and copy the *Value* (not the Secret ID) — it is only shown once.
  </Step>

  <Step>
    ### Add credentials to environment

    Copy the **Application (client) ID** from the app's Overview page and the client secret you just created:

    ```dotenv title=".env"
    PUBLIC_MICROSOFT_CLIENT_ID=<your-application-client-id>
    MICROSOFT_CLIENT_SECRET=<your-client-secret-value>
    ```

    By default, the framework uses Microsoft's `common` tenant, which allows any work, school, or personal Microsoft account to sign in. To restrict sign-in to a specific Entra tenant, pass `tenantId` through your `createAgentsAuth()` configuration in code — there is no env var for tenant scoping.
  </Step>

  <Step>
    ### Restart services

    ```bash
    docker compose up -d
    ```

    The Microsoft sign-in option will now appear on the login page.
  </Step>
</Steps>

## Troubleshooting

### "Invalid credentials" on first login

Verify these environment variables are set correctly:

* `INKEEP_AGENTS_MANAGE_UI_USERNAME` — must be a valid email format
* `INKEEP_AGENTS_MANAGE_UI_PASSWORD` — must be at least 15 characters and include at least one lowercase letter, one uppercase letter, one digit, and one special character

### Google sign-in not appearing or not working

* Ensure both `PUBLIC_GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are set
* Verify the redirect URI in Google Cloud Console matches your app URL exactly

### Microsoft sign-in not appearing or not working

* Ensure both `PUBLIC_MICROSOFT_CLIENT_ID` and `MICROSOFT_CLIENT_SECRET` are set
* Verify the redirect URI in the Azure Portal app registration matches your app URL exactly (path must be `/api/auth/callback/microsoft`)
* If a user belonging to your Entra tenant is refused, confirm the app registration's **Supported account types** setting permits their account kind
* Ensure you copied the client secret's *Value*, not its Secret ID

### Users can't see projects

Organization Members need explicit project-level roles to access projects. Either:

* Assign them a project role via **Project Settings** → **Members**
* Promote them to organization Admin (gives access to all projects)


# Deploy to AWS EC2 (/deployment/aws-ec2)

Deploy to AWS EC2 with Docker Compose



## Create a VM Instance

* Go to [Compute Engine](https://console.aws.amazon.com/ec2/v2/home).
* Launch an instance
* Select Amazon Machine Image (AMI)
* Recommended size is at least `t2.large` (2 vCPU, 8 GiB Memory).
* Click "Edit" in the "Network settings" section. Set up an Inbound Security Group Rules for (TCP, 3000, 0.0.0.0/0), (TCP, 3002-3003, 0.0.0.0/0), (TCP, 3050-3051, 0.0.0.0/0), and (TCP, 3080, 0.0.0.0/0). These are the ports exposed by the Inkeep services.
* Auto-assign public IP
* Increase the size of storage to at least 100 GiB.

## Install Docker Compose

1. SSH into the EC2 Instance

2. Install packages

```bash
sudo dnf update
sudo dnf install -y git
sudo dnf install -y docker
```

```bash
sudo mkdir -p /usr/libexec/docker/cli-plugins
sudo curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m) -o /usr/libexec/docker/cli-plugins/docker-compose
sudo chmod +x /usr/libexec/docker/cli-plugins/docker-compose
```

## Deploy SigNoz and Nango

<>
  Clone this repo, which includes Docker files with SigNoz and Nango:

  ```bash
  git clone https://github.com/inkeep/agents-optional-local-dev inkeep-external-services
  cd inkeep-external-services
  ```

  Run this command to autogenerate a `.env` file from the template `.env.docker.example`:

  ```bash
  ./scripts/generate-env-from-example.sh
  ```

  Nango requires a `NANGO_ENCRYPTION_KEY`. Once you create this, it cannot be edited.

  Here's an overview of the important environment variables when deploying to production. Make sure to review all of these in your `.env` file.

  ```bash
  NANGO_ENCRYPTION_KEY=<REPLACE_WITH_NANGO_ENCRYPTION_KEY>
  SIGNOZ_TOKENIZER_JWT_SECRET=<REPLACE_WITH_SIGNOZ_TOKENIZER_JWT_SECRET>

  # Replace these with your <vm_ip> in production!
  NANGO_SERVER_URL=http://<vm_ip>:3050
  NANGO_PUBLIC_CONNECT_URL=http://<vm_ip>:3051

  # Modify these in production environments!
  NANGO_DASHBOARD_USERNAME=admin@example.com
  NANGO_DASHBOARD_PASSWORD=<REPLACE_WITH_NANGO_DASHBOARD_PASSWORD>
  ```

  Build and deploy SigNoz, Nango, OTEL Collector, and Jaeger:

  ```bash
  docker compose up -d
  ```

  This may take up to 5 minutes to start.

  ### Retrieve your SigNoz and Nango API Keys

  To get your SigNoz API key `SIGNOZ_API_KEY`:

  * Open SigNoz in a browser at `http://<vm_ip>:3080`
  * On first login, you will be prompted to create an admin account.
  * Navigate to Settings → Account Settings → API Keys → New Key
  * Choose a role, Viewer is sufficient for observability
  * Set the expiration field to "No Expiry" to prevent the key from expiring

  <Note>
    By default, the retention period for conversation data and traces is set to **15 days**. To set a longer retention period, navigate to the **General** tab on the **Settings** page in SigNoz.
  </Note>

  To get your Nango secret key `NANGO_SECRET_KEY`:

  * Open Nango in a browser at `http://<vm_ip>:3050`
  * Nango auto-creates two environments, Prod and Dev. Select the one you will use.
  * Navigate to Environment Settings to find the secret key
</>

## Deploy the Inkeep Agent Framework

<>
  From the root directory, create a new project directory for the Docker Compose setup for the Inkeep Agent Framework

  ```bash
  mkdir inkeep && cd inkeep
  wget https://raw.githubusercontent.com/inkeep/agents/refs/heads/main/docker-compose.yml
  wget https://raw.githubusercontent.com/inkeep/agents/refs/heads/main/.env.docker.example
  ```

  Generate a `.env` file from the example:

  ```bash
  cp .env.docker.example .env && \
  \
    # Core secrets
    inkeep_agents_manage_ui_password="$(openssl rand -base64 18)Aa1!" && \
    inkeep_agents_run_api_bypass_secret=$(openssl rand -base64 32) && \
    inkeep_agents_jwt_signing_secret=$(openssl rand -base64 32) && \
    better_auth_secret=$(openssl rand -base64 32) && \
    spicedb_preshared_key=$(openssl rand -base64 32) && \
    jwt_tmp_priv=$(mktemp) && \
    jwt_tmp_pub=$(mktemp) && \
    openssl genrsa -out "$jwt_tmp_priv" 2048 2>/dev/null && \
    openssl rsa -in "$jwt_tmp_priv" -pubout -out "$jwt_tmp_pub" 2>/dev/null && \
    inkeep_agents_temp_jwt_private_key=$(base64 -i "$jwt_tmp_priv" | tr -d '\n') && \
    inkeep_agents_temp_jwt_public_key=$(base64 -i "$jwt_tmp_pub" | tr -d '\n') && \
    rm -f "$jwt_tmp_priv" "$jwt_tmp_pub" && \
    tmp_file=$(mktemp) && \
    sed \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_MANAGE_UI_PASSWORD>|$inkeep_agents_manage_ui_password|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_RUN_API_BYPASS_SECRET>|$inkeep_agents_run_api_bypass_secret|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_JWT_SIGNING_SECRET>|$inkeep_agents_jwt_signing_secret|" \
      -e "s|<REPLACE_WITH_BETTER_AUTH_SECRET>|$better_auth_secret|" \
      -e "s|<REPLACE_WITH_SPICEDB_PRESHARED_KEY>|$spicedb_preshared_key|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_TEMP_JWT_PRIVATE_KEY>|$inkeep_agents_temp_jwt_private_key|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_TEMP_JWT_PUBLIC_KEY>|$inkeep_agents_temp_jwt_public_key|" \
      .env > "$tmp_file" && \
    mv "$tmp_file" .env && \
    echo "✅ Docker .env created with autogenerated secrets."
  ```

  Here's an overview of the important environment variables when deploying to production. Make sure to review all of these in your `.env` file.

  ```bash
  ENVIRONMENT=production

  # (1) AI Provider Keys (you need at least one)
  ANTHROPIC_API_KEY=
  OPENAI_API_KEY=
  GOOGLE_GENERATIVE_AI_API_KEY=
  AZURE_API_KEY=

  # (2) From Nango dashboard at http://<vm_ip>:3050
  NANGO_SECRET_KEY=

  # (3) From SigNoz dashboard at http://<vm_ip>:3080
  SIGNOZ_API_KEY=

  # (4) Set these for the Manage UI at http://<vm_ip>:3000
  PUBLIC_INKEEP_AGENTS_API_URL=http://<vm_ip>:3002
  PUBLIC_NANGO_SERVER_URL=http://<vm_ip>:3050
  PUBLIC_NANGO_CONNECT_BASE_URL=http://<vm_ip>:3051
  PUBLIC_SIGNOZ_URL=http://<vm_ip>:3080

  # (5) Set these for Agents API
  INKEEP_AGENTS_MANAGE_UI_URL=http://<vm_ip>:3000
  INKEEP_AGENTS_API_URL=http://<vm_ip>:3002
  INKEEP_AGENTS_RUN_API_BYPASS_SECRET=<REPLACE_WITH_INKEEP_AGENTS_RUN_API_BYPASS_SECRET>
  ```

  <Tip>
    For long-running agents or custom deployment requirements, you can override runtime limits like execution timeouts, maximum transfers, and generation steps. See [Configure Runtime Limits](/typescript-sdk/configure-runtime-limits) for examples and the complete list of overridable settings.
  </Tip>

  Run with Docker:

  ```bash
  docker compose up -d
  ```

  Then open `http://<vm_ip>:3000` in a browser!
</>

## Upgrading

### Upgrading the Inkeep Agent Platform

<>
  To upgrade the Inkeep Agent Platform to the latest version, run the following commands:

  ```bash
  cd inkeep
  docker compose down
  wget -O docker-compose.yml https://raw.githubusercontent.com/inkeep/agents/main/docker-compose.yml
  docker compose pull
  docker compose up -d --remove-orphans
  ```

  These commands replace your Docker Compose file with the latest version, pull any newer image versions, and restart all the services. Existing database data is preserved, but there will be downtime while the services restart.

  To clean up old unused Docker images and free disk space:

  ```bash
  docker image prune -a
  ```
</>

### Upgrading SigNoz and Nango

<>
  To upgrade Signoz and Nango, run the following commands:

  ```bash
  cd inkeep-external-services
  git pull
  docker compose down
  docker compose pull
  docker compose up -d --remove-orphans
  ```

  These commands pull the latest changes from the repository (including the Docker Compose file), pull any newer image versions, and restart all the services. Existing database data is preserved, but there will be downtime while the services restart.

  To clean up old unused Docker images and free disk space:

  ```bash
  docker image prune -a
  ```
</>


# Deploy to Azure VM (/deployment/azure-vm)

Deploy to Azure VM with Docker Compose



## Create a VM Instance

* Go to [Compute infrastructure | Virtual machines](https://portal.azure.com/#blade/Microsoft_Azure_ComputeHub/ComputeHubMenuBlade/virtualMachinesBrowse) in your Azure Portal.
* Create a virtual machine
* Select a subscription and resource group
* Assign a name for the virtual machine
* Assign a region (e.g. `(US) East US 2`)
* For Availability options, select `No infrastructure redundancy required`
* Use image: `Ubuntu Server 24.04 LTS - x64 Gen2`
* Recommended size is at least `Standard_D2s_v3` (2 vcpus, 8 GiB memory).
* Configure SSH (you will need to SSH into the VM)
* In the "Disks" tab, confirm the OS disk size is at least 128 GiB (P10).
* In the "Networking" tab, assign public IP address
* Click on "Review + create"
* After the VM is running, go to the "Network Settings" page under "Networking"
* Add inbound security rule for TCP ports: 3000,3002,3050,3051,3080

## Install Docker Compose

1. [SSH into the VM](https://learn.microsoft.com/en-us/azure/virtual-machines/linux-vm-connect?tabs=Linux)

2. [Set up Docker's apt repository](https://docs.docker.com/engine/install/ubuntu/)

```bash
# Add Docker's official GPG key:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
```

3. Install the Docker packages

```bash
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

4. Grant permissions

```
sudo usermod -aG docker $USER
newgrp docker
```

## Deploy SigNoz and Nango

<>
  Clone this repo, which includes Docker files with SigNoz and Nango:

  ```bash
  git clone https://github.com/inkeep/agents-optional-local-dev inkeep-external-services
  cd inkeep-external-services
  ```

  Run this command to autogenerate a `.env` file from the template `.env.docker.example`:

  ```bash
  ./scripts/generate-env-from-example.sh
  ```

  Nango requires a `NANGO_ENCRYPTION_KEY`. Once you create this, it cannot be edited.

  Here's an overview of the important environment variables when deploying to production. Make sure to review all of these in your `.env` file.

  ```bash
  NANGO_ENCRYPTION_KEY=<REPLACE_WITH_NANGO_ENCRYPTION_KEY>
  SIGNOZ_TOKENIZER_JWT_SECRET=<REPLACE_WITH_SIGNOZ_TOKENIZER_JWT_SECRET>

  # Replace these with your <vm_ip> in production!
  NANGO_SERVER_URL=http://<vm_ip>:3050
  NANGO_PUBLIC_CONNECT_URL=http://<vm_ip>:3051

  # Modify these in production environments!
  NANGO_DASHBOARD_USERNAME=admin@example.com
  NANGO_DASHBOARD_PASSWORD=<REPLACE_WITH_NANGO_DASHBOARD_PASSWORD>
  ```

  Build and deploy SigNoz, Nango, OTEL Collector, and Jaeger:

  ```bash
  docker compose up -d
  ```

  This may take up to 5 minutes to start.

  ### Retrieve your SigNoz and Nango API Keys

  To get your SigNoz API key `SIGNOZ_API_KEY`:

  * Open SigNoz in a browser at `http://<vm_ip>:3080`
  * On first login, you will be prompted to create an admin account.
  * Navigate to Settings → Account Settings → API Keys → New Key
  * Choose a role, Viewer is sufficient for observability
  * Set the expiration field to "No Expiry" to prevent the key from expiring

  <Note>
    By default, the retention period for conversation data and traces is set to **15 days**. To set a longer retention period, navigate to the **General** tab on the **Settings** page in SigNoz.
  </Note>

  To get your Nango secret key `NANGO_SECRET_KEY`:

  * Open Nango in a browser at `http://<vm_ip>:3050`
  * Nango auto-creates two environments, Prod and Dev. Select the one you will use.
  * Navigate to Environment Settings to find the secret key
</>

## Deploy the Inkeep Agent Framework

<>
  From the root directory, create a new project directory for the Docker Compose setup for the Inkeep Agent Framework

  ```bash
  mkdir inkeep && cd inkeep
  wget https://raw.githubusercontent.com/inkeep/agents/refs/heads/main/docker-compose.yml
  wget https://raw.githubusercontent.com/inkeep/agents/refs/heads/main/.env.docker.example
  ```

  Generate a `.env` file from the example:

  ```bash
  cp .env.docker.example .env && \
  \
    # Core secrets
    inkeep_agents_manage_ui_password="$(openssl rand -base64 18)Aa1!" && \
    inkeep_agents_run_api_bypass_secret=$(openssl rand -base64 32) && \
    inkeep_agents_jwt_signing_secret=$(openssl rand -base64 32) && \
    better_auth_secret=$(openssl rand -base64 32) && \
    spicedb_preshared_key=$(openssl rand -base64 32) && \
    jwt_tmp_priv=$(mktemp) && \
    jwt_tmp_pub=$(mktemp) && \
    openssl genrsa -out "$jwt_tmp_priv" 2048 2>/dev/null && \
    openssl rsa -in "$jwt_tmp_priv" -pubout -out "$jwt_tmp_pub" 2>/dev/null && \
    inkeep_agents_temp_jwt_private_key=$(base64 -i "$jwt_tmp_priv" | tr -d '\n') && \
    inkeep_agents_temp_jwt_public_key=$(base64 -i "$jwt_tmp_pub" | tr -d '\n') && \
    rm -f "$jwt_tmp_priv" "$jwt_tmp_pub" && \
    tmp_file=$(mktemp) && \
    sed \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_MANAGE_UI_PASSWORD>|$inkeep_agents_manage_ui_password|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_RUN_API_BYPASS_SECRET>|$inkeep_agents_run_api_bypass_secret|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_JWT_SIGNING_SECRET>|$inkeep_agents_jwt_signing_secret|" \
      -e "s|<REPLACE_WITH_BETTER_AUTH_SECRET>|$better_auth_secret|" \
      -e "s|<REPLACE_WITH_SPICEDB_PRESHARED_KEY>|$spicedb_preshared_key|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_TEMP_JWT_PRIVATE_KEY>|$inkeep_agents_temp_jwt_private_key|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_TEMP_JWT_PUBLIC_KEY>|$inkeep_agents_temp_jwt_public_key|" \
      .env > "$tmp_file" && \
    mv "$tmp_file" .env && \
    echo "✅ Docker .env created with autogenerated secrets."
  ```

  Here's an overview of the important environment variables when deploying to production. Make sure to review all of these in your `.env` file.

  ```bash
  ENVIRONMENT=production

  # (1) AI Provider Keys (you need at least one)
  ANTHROPIC_API_KEY=
  OPENAI_API_KEY=
  GOOGLE_GENERATIVE_AI_API_KEY=
  AZURE_API_KEY=

  # (2) From Nango dashboard at http://<vm_ip>:3050
  NANGO_SECRET_KEY=

  # (3) From SigNoz dashboard at http://<vm_ip>:3080
  SIGNOZ_API_KEY=

  # (4) Set these for the Manage UI at http://<vm_ip>:3000
  PUBLIC_INKEEP_AGENTS_API_URL=http://<vm_ip>:3002
  PUBLIC_NANGO_SERVER_URL=http://<vm_ip>:3050
  PUBLIC_NANGO_CONNECT_BASE_URL=http://<vm_ip>:3051
  PUBLIC_SIGNOZ_URL=http://<vm_ip>:3080

  # (5) Set these for Agents API
  INKEEP_AGENTS_MANAGE_UI_URL=http://<vm_ip>:3000
  INKEEP_AGENTS_API_URL=http://<vm_ip>:3002
  INKEEP_AGENTS_RUN_API_BYPASS_SECRET=<REPLACE_WITH_INKEEP_AGENTS_RUN_API_BYPASS_SECRET>
  ```

  <Tip>
    For long-running agents or custom deployment requirements, you can override runtime limits like execution timeouts, maximum transfers, and generation steps. See [Configure Runtime Limits](/typescript-sdk/configure-runtime-limits) for examples and the complete list of overridable settings.
  </Tip>

  Run with Docker:

  ```bash
  docker compose up -d
  ```

  Then open `http://<vm_ip>:3000` in a browser!
</>

## Production

For a production deployment, you will want to restrict access to the VM and only expose 3002. To do this, we'll setup an Application Gateway and a Front Door (with https).

### Application Gateway

* On the VM page, under "Networking" > "Load balancing", click on "Add load balancing" and create a new "Application Gateway"
* Provide a name for the gateway and a name for the routing rule. The routing rule should be configured to Port 80 HTTP.
* Wait for the gateway to deploy (can take >5 mins)
* On the gateway page, under "Settings" > "Backend pools", click on the pool. Under "Backend targets", add a target of type "Virtual machine" and select your VM.
* On the gateway page, under "Settings" > "Backend Settings", change the "Backend port" to 3002.
* On the gateway page, under "Settings" > "Health probes", add a new health probe. Provide a name, select HTTP for protocol. For the host use `127.0.0.1`, `3002` for the port, and `/health` for the path. "Pick host name from backend settings" and "Pick port from backend settings" should be "No". Then select your "Backend settings".

### Front Door

* Create an Azure Front Door. Continue with "Quick create".
* For the origin, select the "Application Gateway" you created.
* Disable caching.
* You will be assigned an autogenerated url with https from Azure. To add a custom domain, go to "Settings" > "Domains".

### Restrict VM Access

* Modify the inbound security rules, for example, to only specific IPs. You will need this for building agents.
* All external access should only use the gateway and front door.

## Upgrading

### Upgrading the Inkeep Agent Platform

<>
  To upgrade the Inkeep Agent Platform to the latest version, run the following commands:

  ```bash
  cd inkeep
  docker compose down
  wget -O docker-compose.yml https://raw.githubusercontent.com/inkeep/agents/main/docker-compose.yml
  docker compose pull
  docker compose up -d --remove-orphans
  ```

  These commands replace your Docker Compose file with the latest version, pull any newer image versions, and restart all the services. Existing database data is preserved, but there will be downtime while the services restart.

  To clean up old unused Docker images and free disk space:

  ```bash
  docker image prune -a
  ```
</>

### Upgrading SigNoz and Nango

<>
  To upgrade Signoz and Nango, run the following commands:

  ```bash
  cd inkeep-external-services
  git pull
  docker compose down
  docker compose pull
  docker compose up -d --remove-orphans
  ```

  These commands pull the latest changes from the repository (including the Docker Compose file), pull any newer image versions, and restart all the services. Existing database data is preserved, but there will be downtime while the services restart.

  To clean up old unused Docker images and free disk space:

  ```bash
  docker image prune -a
  ```
</>


# Build a Custom Docker Image (/deployment/docker-build)

How to build your own Docker images



If you created a project from the quick start, the template includes a set of Dockerfiles and `docker-compose.yml` files.

To build and run locally:

```sh
docker compose build
docker compose up -d
```


# Deploy using Docker (Local Development) (/deployment/docker-local)

Deploy locally with Docker Compose



## Install Docker

* [Install Docker Desktop](https://www.docker.com/products/docker-desktop/)


# Configure Email (/deployment/email)

Set up SMTP email for team invitations and password resets



Configure email delivery so the platform can send team invitation emails and password reset links. Without email configured, these features degrade gracefully — invitation links are shown directly in the UI and password reset is disabled.

<Note>
  For a feature overview of invitations and password reset, see [Access Control](/visual-builder/access-control).
</Note>

## How It Works

The email system uses a priority-based transport selection:

| Priority | Transport        | When used                                      |
| -------- | ---------------- | ---------------------------------------------- |
| 1        | **Resend**       | `RESEND_API_KEY` is set                        |
| 2        | **Generic SMTP** | `SMTP_HOST` is set (and no Resend key)         |
| 3        | **Disabled**     | Neither is set — email features are turned off |

Both transports require `SMTP_FROM_ADDRESS` to be set. If the from address is missing, email is disabled even when a transport is configured.

## Environment Variables

| Variable            | Required | Description                                                    |
| ------------------- | -------- | -------------------------------------------------------------- |
| `SMTP_FROM_ADDRESS` | Yes      | Sender email address (e.g., `noreply@example.com`)             |
| `SMTP_FROM_NAME`    | No       | Sender display name (e.g., `Inkeep`). Defaults to the address. |
| `SMTP_REPLY_TO`     | No       | Reply-to address. Defaults to the from address.                |

### Option A: Resend (Recommended for Production)

[Resend](https://resend.com) provides a managed email delivery service with high deliverability.

| Variable         | Required | Description         |
| ---------------- | -------- | ------------------- |
| `RESEND_API_KEY` | Yes      | Your Resend API key |

```dotenv title=".env"
RESEND_API_KEY=re_your_api_key
SMTP_FROM_ADDRESS=notifications@yourdomain.com
SMTP_FROM_NAME=Inkeep
```

<Tip>
  When using Resend, you don't need to configure any SMTP host or port variables — the system connects to Resend's SMTP relay automatically.
</Tip>

### Option B: Generic SMTP

Use any SMTP server (Amazon SES, SendGrid, Gmail, self-hosted Postfix, etc.).

| Variable        | Required | Description                                                            |
| --------------- | -------- | ---------------------------------------------------------------------- |
| `SMTP_HOST`     | Yes      | SMTP server hostname                                                   |
| `SMTP_PORT`     | No       | Server port. Default: `587`                                            |
| `SMTP_SECURE`   | No       | Use TLS/SSL. Auto-detected from port if not set (`true` for port 465). |
| `SMTP_USER`     | No       | SMTP authentication username                                           |
| `SMTP_PASSWORD` | No       | SMTP authentication password                                           |

```dotenv title=".env"
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=apikey
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_ADDRESS=noreply@example.com
SMTP_FROM_NAME=Inkeep
```

## Local Development with Mailpit

[Mailpit](https://github.com/axllent/mailpit) captures outgoing email locally without sending it to real recipients. This is the recommended setup for development and testing.

Mailpit is included in the default Docker Compose configuration. Configure the SMTP environment variables to point to it:

<Steps>
  <Step>
    ### Configure environment variables

    ```dotenv title=".env"
    SMTP_HOST=localhost
    SMTP_PORT=1025
    SMTP_SECURE=false
    SMTP_FROM_ADDRESS=noreply@inkeep.local
    SMTP_FROM_NAME=Inkeep
    ```
  </Step>

  <Step>
    ### Start services and verify

    ```bash
    docker compose up -d
    ```

    Open [http://localhost:8025](http://localhost:8025) to view the Mailpit inbox. Any emails sent by the platform (invitations, password resets) will appear here.
  </Step>
</Steps>

## What Happens Without Email

When email is not configured, the platform adjusts its behavior:

| Feature              | With email                             | Without email                                        |
| -------------------- | -------------------------------------- | ---------------------------------------------------- |
| **Team invitations** | Invitation email sent to the recipient | Invitation link shown directly in the UI             |
| **Password reset**   | Reset link emailed to the user         | "Forgot password?" link hidden from the sign-in page |

No errors are thrown — the UI adapts automatically based on whether email is available.

## Verifying Email Configuration

After configuring your environment variables, restart services and test:

1. Go to **Settings** in the sidebar
2. Click **Invite** and send a test invitation
3. Verify the email arrives (in Mailpit for local dev, or the real inbox for production)

If using Mailpit, open [http://localhost:8025](http://localhost:8025) to see the captured email.

## Troubleshooting

### Emails not being sent

* Verify `SMTP_FROM_ADDRESS` is set — this is required for all transports
* Check the API server logs for `[email]` warnings about missing configuration
* For Resend: confirm your API key is valid and the from address domain is verified in Resend
* For generic SMTP: verify the host, port, and credentials are correct

### Invitation link shown instead of email

This is expected behavior when email is not configured. Set up SMTP or Resend to enable email delivery.

### "Password reset unavailable" message

The forgot-password page shows this when email is not configured. Configure email to enable self-service password resets.


# Deploy to GCP Compute Engine (/deployment/gcp-compute-engine)

Deploy to GCP Compute Engine with Docker Compose



## Create a VM Instance

* Go to [Compute Engine](https://console.cloud.google.com/compute/instances) in your GCP project.
* Create an instance; a recommended size is at least `e2-standard-2` (2 vCPU, 1 core, 8 GB memory).
* Use Debian GNU/Linux 12 (bookworm)
* Increase the size of the boot disk to 100 GB.
* Allow HTTP traffic.
* Allow ingress traffic from source IPv4 ranges `0.0.0.0/0` to TCP ports: `3000, 3002, 3003, 3050, 3051, 3080`. These are the ports exposed by the Inkeep services.
* Retrieve an external IP address (if applicable, set up a static IP or set up a load balancer).

## Install Docker Compose

1. [SSH into the VM](https://cloud.google.com/compute/docs/connect/standard-ssh)

2. [Set up Docker's apt repository](https://docs.docker.com/engine/install/debian/#install-using-the-repository)

```bash
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
```

3. Install the Docker packages

```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

4. Grant permissions

```
sudo usermod -aG docker $USER
newgrp docker
```

## Deploy SigNoz and Nango

<>
  Clone this repo, which includes Docker files with SigNoz and Nango:

  ```bash
  git clone https://github.com/inkeep/agents-optional-local-dev inkeep-external-services
  cd inkeep-external-services
  ```

  Run this command to autogenerate a `.env` file from the template `.env.docker.example`:

  ```bash
  ./scripts/generate-env-from-example.sh
  ```

  Nango requires a `NANGO_ENCRYPTION_KEY`. Once you create this, it cannot be edited.

  Here's an overview of the important environment variables when deploying to production. Make sure to review all of these in your `.env` file.

  ```bash
  NANGO_ENCRYPTION_KEY=<REPLACE_WITH_NANGO_ENCRYPTION_KEY>
  SIGNOZ_TOKENIZER_JWT_SECRET=<REPLACE_WITH_SIGNOZ_TOKENIZER_JWT_SECRET>

  # Replace these with your <vm_ip> in production!
  NANGO_SERVER_URL=http://<vm_ip>:3050
  NANGO_PUBLIC_CONNECT_URL=http://<vm_ip>:3051

  # Modify these in production environments!
  NANGO_DASHBOARD_USERNAME=admin@example.com
  NANGO_DASHBOARD_PASSWORD=<REPLACE_WITH_NANGO_DASHBOARD_PASSWORD>
  ```

  Build and deploy SigNoz, Nango, OTEL Collector, and Jaeger:

  ```bash
  docker compose up -d
  ```

  This may take up to 5 minutes to start.

  ### Retrieve your SigNoz and Nango API Keys

  To get your SigNoz API key `SIGNOZ_API_KEY`:

  * Open SigNoz in a browser at `http://<vm_ip>:3080`
  * On first login, you will be prompted to create an admin account.
  * Navigate to Settings → Account Settings → API Keys → New Key
  * Choose a role, Viewer is sufficient for observability
  * Set the expiration field to "No Expiry" to prevent the key from expiring

  <Note>
    By default, the retention period for conversation data and traces is set to **15 days**. To set a longer retention period, navigate to the **General** tab on the **Settings** page in SigNoz.
  </Note>

  To get your Nango secret key `NANGO_SECRET_KEY`:

  * Open Nango in a browser at `http://<vm_ip>:3050`
  * Nango auto-creates two environments, Prod and Dev. Select the one you will use.
  * Navigate to Environment Settings to find the secret key
</>

## Deploy the Inkeep Agent Framework

<>
  From the root directory, create a new project directory for the Docker Compose setup for the Inkeep Agent Framework

  ```bash
  mkdir inkeep && cd inkeep
  wget https://raw.githubusercontent.com/inkeep/agents/refs/heads/main/docker-compose.yml
  wget https://raw.githubusercontent.com/inkeep/agents/refs/heads/main/.env.docker.example
  ```

  Generate a `.env` file from the example:

  ```bash
  cp .env.docker.example .env && \
  \
    # Core secrets
    inkeep_agents_manage_ui_password="$(openssl rand -base64 18)Aa1!" && \
    inkeep_agents_run_api_bypass_secret=$(openssl rand -base64 32) && \
    inkeep_agents_jwt_signing_secret=$(openssl rand -base64 32) && \
    better_auth_secret=$(openssl rand -base64 32) && \
    spicedb_preshared_key=$(openssl rand -base64 32) && \
    jwt_tmp_priv=$(mktemp) && \
    jwt_tmp_pub=$(mktemp) && \
    openssl genrsa -out "$jwt_tmp_priv" 2048 2>/dev/null && \
    openssl rsa -in "$jwt_tmp_priv" -pubout -out "$jwt_tmp_pub" 2>/dev/null && \
    inkeep_agents_temp_jwt_private_key=$(base64 -i "$jwt_tmp_priv" | tr -d '\n') && \
    inkeep_agents_temp_jwt_public_key=$(base64 -i "$jwt_tmp_pub" | tr -d '\n') && \
    rm -f "$jwt_tmp_priv" "$jwt_tmp_pub" && \
    tmp_file=$(mktemp) && \
    sed \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_MANAGE_UI_PASSWORD>|$inkeep_agents_manage_ui_password|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_RUN_API_BYPASS_SECRET>|$inkeep_agents_run_api_bypass_secret|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_JWT_SIGNING_SECRET>|$inkeep_agents_jwt_signing_secret|" \
      -e "s|<REPLACE_WITH_BETTER_AUTH_SECRET>|$better_auth_secret|" \
      -e "s|<REPLACE_WITH_SPICEDB_PRESHARED_KEY>|$spicedb_preshared_key|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_TEMP_JWT_PRIVATE_KEY>|$inkeep_agents_temp_jwt_private_key|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_TEMP_JWT_PUBLIC_KEY>|$inkeep_agents_temp_jwt_public_key|" \
      .env > "$tmp_file" && \
    mv "$tmp_file" .env && \
    echo "✅ Docker .env created with autogenerated secrets."
  ```

  Here's an overview of the important environment variables when deploying to production. Make sure to review all of these in your `.env` file.

  ```bash
  ENVIRONMENT=production

  # (1) AI Provider Keys (you need at least one)
  ANTHROPIC_API_KEY=
  OPENAI_API_KEY=
  GOOGLE_GENERATIVE_AI_API_KEY=
  AZURE_API_KEY=

  # (2) From Nango dashboard at http://<vm_ip>:3050
  NANGO_SECRET_KEY=

  # (3) From SigNoz dashboard at http://<vm_ip>:3080
  SIGNOZ_API_KEY=

  # (4) Set these for the Manage UI at http://<vm_ip>:3000
  PUBLIC_INKEEP_AGENTS_API_URL=http://<vm_ip>:3002
  PUBLIC_NANGO_SERVER_URL=http://<vm_ip>:3050
  PUBLIC_NANGO_CONNECT_BASE_URL=http://<vm_ip>:3051
  PUBLIC_SIGNOZ_URL=http://<vm_ip>:3080

  # (5) Set these for Agents API
  INKEEP_AGENTS_MANAGE_UI_URL=http://<vm_ip>:3000
  INKEEP_AGENTS_API_URL=http://<vm_ip>:3002
  INKEEP_AGENTS_RUN_API_BYPASS_SECRET=<REPLACE_WITH_INKEEP_AGENTS_RUN_API_BYPASS_SECRET>
  ```

  <Tip>
    For long-running agents or custom deployment requirements, you can override runtime limits like execution timeouts, maximum transfers, and generation steps. See [Configure Runtime Limits](/typescript-sdk/configure-runtime-limits) for examples and the complete list of overridable settings.
  </Tip>

  Run with Docker:

  ```bash
  docker compose up -d
  ```

  Then open `http://<vm_ip>:3000` in a browser!
</>

## Upgrading

### Upgrading the Inkeep Agent Platform

<>
  To upgrade the Inkeep Agent Platform to the latest version, run the following commands:

  ```bash
  cd inkeep
  docker compose down
  wget -O docker-compose.yml https://raw.githubusercontent.com/inkeep/agents/main/docker-compose.yml
  docker compose pull
  docker compose up -d --remove-orphans
  ```

  These commands replace your Docker Compose file with the latest version, pull any newer image versions, and restart all the services. Existing database data is preserved, but there will be downtime while the services restart.

  To clean up old unused Docker images and free disk space:

  ```bash
  docker image prune -a
  ```
</>

### Upgrading SigNoz and Nango

<>
  To upgrade Signoz and Nango, run the following commands:

  ```bash
  cd inkeep-external-services
  git pull
  docker compose down
  docker compose pull
  docker compose up -d --remove-orphans
  ```

  These commands pull the latest changes from the repository (including the Docker Compose file), pull any newer image versions, and restart all the services. Existing database data is preserved, but there will be downtime while the services restart.

  To clean up old unused Docker images and free disk space:

  ```bash
  docker image prune -a
  ```
</>


# Deploy to Hetzner (/deployment/hetzner)

Deploy to Hetzner with Docker Compose



## Create a server

* Create a server, recommended size is at least CPX32 (4 VCPUS, 8 GB RAM, >100 GB Storage)
* Select Ubuntu 24.04 Image
* Create an inbound firewall rule to allow TCP ports: 3000, 3002, 3003, 3050, 3051, and 3080. These are the ports exposed by the Inkeep services.

## Install Docker Compose

1. SSH into the server as root

2. [Set up Docker's apt repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)

```bash
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
```

3. Install the Docker packages

```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

## Deploy SigNoz and Nango

<>
  Clone this repo, which includes Docker files with SigNoz and Nango:

  ```bash
  git clone https://github.com/inkeep/agents-optional-local-dev inkeep-external-services
  cd inkeep-external-services
  ```

  Run this command to autogenerate a `.env` file from the template `.env.docker.example`:

  ```bash
  ./scripts/generate-env-from-example.sh
  ```

  Nango requires a `NANGO_ENCRYPTION_KEY`. Once you create this, it cannot be edited.

  Here's an overview of the important environment variables when deploying to production. Make sure to review all of these in your `.env` file.

  ```bash
  NANGO_ENCRYPTION_KEY=<REPLACE_WITH_NANGO_ENCRYPTION_KEY>
  SIGNOZ_TOKENIZER_JWT_SECRET=<REPLACE_WITH_SIGNOZ_TOKENIZER_JWT_SECRET>

  # Replace these with your <vm_ip> in production!
  NANGO_SERVER_URL=http://<vm_ip>:3050
  NANGO_PUBLIC_CONNECT_URL=http://<vm_ip>:3051

  # Modify these in production environments!
  NANGO_DASHBOARD_USERNAME=admin@example.com
  NANGO_DASHBOARD_PASSWORD=<REPLACE_WITH_NANGO_DASHBOARD_PASSWORD>
  ```

  Build and deploy SigNoz, Nango, OTEL Collector, and Jaeger:

  ```bash
  docker compose up -d
  ```

  This may take up to 5 minutes to start.

  ### Retrieve your SigNoz and Nango API Keys

  To get your SigNoz API key `SIGNOZ_API_KEY`:

  * Open SigNoz in a browser at `http://<vm_ip>:3080`
  * On first login, you will be prompted to create an admin account.
  * Navigate to Settings → Account Settings → API Keys → New Key
  * Choose a role, Viewer is sufficient for observability
  * Set the expiration field to "No Expiry" to prevent the key from expiring

  <Note>
    By default, the retention period for conversation data and traces is set to **15 days**. To set a longer retention period, navigate to the **General** tab on the **Settings** page in SigNoz.
  </Note>

  To get your Nango secret key `NANGO_SECRET_KEY`:

  * Open Nango in a browser at `http://<vm_ip>:3050`
  * Nango auto-creates two environments, Prod and Dev. Select the one you will use.
  * Navigate to Environment Settings to find the secret key
</>

## Deploy the Inkeep Agent Framework

<>
  From the root directory, create a new project directory for the Docker Compose setup for the Inkeep Agent Framework

  ```bash
  mkdir inkeep && cd inkeep
  wget https://raw.githubusercontent.com/inkeep/agents/refs/heads/main/docker-compose.yml
  wget https://raw.githubusercontent.com/inkeep/agents/refs/heads/main/.env.docker.example
  ```

  Generate a `.env` file from the example:

  ```bash
  cp .env.docker.example .env && \
  \
    # Core secrets
    inkeep_agents_manage_ui_password="$(openssl rand -base64 18)Aa1!" && \
    inkeep_agents_run_api_bypass_secret=$(openssl rand -base64 32) && \
    inkeep_agents_jwt_signing_secret=$(openssl rand -base64 32) && \
    better_auth_secret=$(openssl rand -base64 32) && \
    spicedb_preshared_key=$(openssl rand -base64 32) && \
    jwt_tmp_priv=$(mktemp) && \
    jwt_tmp_pub=$(mktemp) && \
    openssl genrsa -out "$jwt_tmp_priv" 2048 2>/dev/null && \
    openssl rsa -in "$jwt_tmp_priv" -pubout -out "$jwt_tmp_pub" 2>/dev/null && \
    inkeep_agents_temp_jwt_private_key=$(base64 -i "$jwt_tmp_priv" | tr -d '\n') && \
    inkeep_agents_temp_jwt_public_key=$(base64 -i "$jwt_tmp_pub" | tr -d '\n') && \
    rm -f "$jwt_tmp_priv" "$jwt_tmp_pub" && \
    tmp_file=$(mktemp) && \
    sed \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_MANAGE_UI_PASSWORD>|$inkeep_agents_manage_ui_password|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_RUN_API_BYPASS_SECRET>|$inkeep_agents_run_api_bypass_secret|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_JWT_SIGNING_SECRET>|$inkeep_agents_jwt_signing_secret|" \
      -e "s|<REPLACE_WITH_BETTER_AUTH_SECRET>|$better_auth_secret|" \
      -e "s|<REPLACE_WITH_SPICEDB_PRESHARED_KEY>|$spicedb_preshared_key|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_TEMP_JWT_PRIVATE_KEY>|$inkeep_agents_temp_jwt_private_key|" \
      -e "s|<REPLACE_WITH_INKEEP_AGENTS_TEMP_JWT_PUBLIC_KEY>|$inkeep_agents_temp_jwt_public_key|" \
      .env > "$tmp_file" && \
    mv "$tmp_file" .env && \
    echo "✅ Docker .env created with autogenerated secrets."
  ```

  Here's an overview of the important environment variables when deploying to production. Make sure to review all of these in your `.env` file.

  ```bash
  ENVIRONMENT=production

  # (1) AI Provider Keys (you need at least one)
  ANTHROPIC_API_KEY=
  OPENAI_API_KEY=
  GOOGLE_GENERATIVE_AI_API_KEY=
  AZURE_API_KEY=

  # (2) From Nango dashboard at http://<vm_ip>:3050
  NANGO_SECRET_KEY=

  # (3) From SigNoz dashboard at http://<vm_ip>:3080
  SIGNOZ_API_KEY=

  # (4) Set these for the Manage UI at http://<vm_ip>:3000
  PUBLIC_INKEEP_AGENTS_API_URL=http://<vm_ip>:3002
  PUBLIC_NANGO_SERVER_URL=http://<vm_ip>:3050
  PUBLIC_NANGO_CONNECT_BASE_URL=http://<vm_ip>:3051
  PUBLIC_SIGNOZ_URL=http://<vm_ip>:3080

  # (5) Set these for Agents API
  INKEEP_AGENTS_MANAGE_UI_URL=http://<vm_ip>:3000
  INKEEP_AGENTS_API_URL=http://<vm_ip>:3002
  INKEEP_AGENTS_RUN_API_BYPASS_SECRET=<REPLACE_WITH_INKEEP_AGENTS_RUN_API_BYPASS_SECRET>
  ```

  <Tip>
    For long-running agents or custom deployment requirements, you can override runtime limits like execution timeouts, maximum transfers, and generation steps. See [Configure Runtime Limits](/typescript-sdk/configure-runtime-limits) for examples and the complete list of overridable settings.
  </Tip>

  Run with Docker:

  ```bash
  docker compose up -d
  ```

  Then open `http://<vm_ip>:3000` in a browser!
</>

## Upgrading

### Upgrading the Inkeep Agent Platform

<>
  To upgrade the Inkeep Agent Platform to the latest version, run the following commands:

  ```bash
  cd inkeep
  docker compose down
  wget -O docker-compose.yml https://raw.githubusercontent.com/inkeep/agents/main/docker-compose.yml
  docker compose pull
  docker compose up -d --remove-orphans
  ```

  These commands replace your Docker Compose file with the latest version, pull any newer image versions, and restart all the services. Existing database data is preserved, but there will be downtime while the services restart.

  To clean up old unused Docker images and free disk space:

  ```bash
  docker image prune -a
  ```
</>

### Upgrading SigNoz and Nango

<>
  To upgrade Signoz and Nango, run the following commands:

  ```bash
  cd inkeep-external-services
  git pull
  docker compose down
  docker compose pull
  docker compose up -d --remove-orphans
  ```

  These commands pull the latest changes from the repository (including the Docker Compose file), pull any newer image versions, and restart all the services. Existing database data is preserved, but there will be downtime while the services restart.

  To clean up old unused Docker images and free disk space:

  ```bash
  docker image prune -a
  ```
</>


# Datadog (/deployment/add-other-services/datadog-apm)

Add Datadog APM to your Inkeep Agent Framework services



## Overview

Learn how to add Datadog APM to your Inkeep Agent Framework services.

## Step 1: Install Datadog APM

From the root of your workspace, run the following command:

```bash
pnpm install dd-trace
```

## Step 2: Set up tracer.ts

In `apps/agents-api` create a new file called `tracer.ts` and add the following code:

```typescript
import tracer from "dd-trace";
tracer.init(); // initialized in a different file to avoid hoisting.
export default tracer;
```

In `apps/run-api/src/index.ts` and `apps/manage-api/src/index.ts` add the following code to the top of the file before all other imports:

```typescript
import "./tracer";
```

## Additional Resources

For more information on how to configure APM, you can consult the official Datadog Node.js [documentation](https://app.datadoghq.com/apm/service-setup?architecture=host-based\&framework=typescript\&language=node\&product=apm).


# S3 Blob Storage (/deployment/add-other-services/s3-blob-storage)

Configure S3 for file attachment storage with presigned URL delivery



## Overview

The Inkeep Agent Framework stores file attachments (images, PDFs, documents) uploaded during conversations. By default, files are stored on the local filesystem. For production deployments, configure S3 (or an S3-compatible service) for durable storage with presigned URL delivery.

When S3 is configured, media URLs in conversation responses are **presigned S3 URLs** — clients fetch files directly from S3 with no proxy overhead. This provides:

* **Zero-proxy delivery** — clients talk directly to S3, no function invocations for media reads
* **Domain isolation** — media is served from `*.s3.amazonaws.com`, separate from your API domain
* **Time-limited access** — presigned URLs expire after 2 hours (configurable via `BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS`)

When S3 is not configured, the framework falls back to Vercel Blob (if `BLOB_READ_WRITE_TOKEN` is set) or the local filesystem. Both Vercel Blob and local storage serve files through a server-side media proxy rather than presigned URLs.

## Step 1: Create an S3 Bucket

Create a private S3 bucket in your preferred AWS region. No public access or special bucket policies are required — all access uses presigned URLs generated server-side.

```bash
aws s3 mb s3://your-media-bucket --region us-east-1
```

## Step 2: Create IAM Credentials

Create an IAM user or role with the following permissions on your bucket:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-media-bucket/*"
    }
  ]
}
```

## Step 3: Configure Environment Variables

Add the following to your `.env` file:

```bash
BLOB_STORAGE_S3_BUCKET=your-media-bucket
BLOB_STORAGE_S3_REGION=us-east-1
BLOB_STORAGE_S3_ACCESS_KEY_ID=your-access-key-id
BLOB_STORAGE_S3_SECRET_ACCESS_KEY=your-secret-access-key
```

All four variables are required when S3 is enabled.

### S3-Compatible Services

For S3-compatible services, set `BLOB_STORAGE_S3_ENDPOINT` and enable path-style access where required:

| Service                 | `BLOB_STORAGE_S3_ENDPOINT`                      | `BLOB_STORAGE_S3_FORCE_PATH_STYLE` |
| ----------------------- | ----------------------------------------------- | ---------------------------------- |
| **Cloudflare R2**       | `https://<account-id>.r2.cloudflarestorage.com` | Not needed (virtual-hosted style)  |
| **DigitalOcean Spaces** | `https://<region>.digitaloceanspaces.com`       | Not needed                         |
| **Backblaze B2**        | `https://s3.<region>.backblazeb2.com`           | `true`                             |

### Optional: Presigned URL Expiry

By default, presigned URLs expire after 2 hours (7200 seconds). To customize:

```bash
BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS=900  # 15 minutes
```

Accepted range: 60–604800 (1 minute to 7 days).

## Storage Backend Priority

The framework selects a storage backend based on which environment variables are set:

| Priority | Backend              | Condition                                |
| -------- | -------------------- | ---------------------------------------- |
| 1        | **S3**               | `BLOB_STORAGE_S3_BUCKET` is set          |
| 2        | **Vercel Blob**      | `BLOB_READ_WRITE_TOKEN` is set           |
| 3        | **Local filesystem** | Neither is set (default, local dev only) |

<Callout type="warning">
  Changing the storage backend after files have been uploaded will make previously stored files inaccessible. Plan your storage backend before going to production.
</Callout>

## How It Works

1. When a user sends a file attachment in a chat message, the file is uploaded to S3 using the configured credentials
2. The file's location is stored in the database as an internal blob URI
3. When conversation history is retrieved, blob URIs are resolved to presigned S3 URLs (default 2-hour expiry, configurable via `BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS`)
4. Clients fetch files directly from S3 using the presigned URL — no proxy needed


# Sentry (/deployment/add-other-services/sentry)

Add Sentry monitoring to your agent services



## Overview

Learn how to add Sentry monitoring to your Inkeep Agent Framework services.

## Step 1: Install Sentry

```bash
pnpm install @sentry/node
```

## Step 2: Update your `.env` file

Add your Sentry DSN to the `.env` file in the root of your workspace.

```bash
SENTRY_DSN=https://<your-sentry-dsn>@sentry.io/<your-sentry-project>
```

## Step 3: Configure Sentry

In `apps/agents-api` create a new file called `sentry.ts` and add the following code:

```typescript
import * as Sentry from "@sentry/node";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  sampleRate: 1.0,
  tracesSampleRate: 1.0,
});
```

In `apps/agents-api/src/index.ts` add the following code to the top of the file before all other imports:

```typescript
import "./sentry";
```

## Forward Error Logs to Sentry

You can use [pino-sentry-transport](https://github.com/tomer-yechiel/pino-sentry-transport) to forward error logs to Sentry.

### Step 1: Install pino-sentry-transport

```bash
pnpm install pino-sentry-transport
```

### Step 2: Configure pino-sentry-transport

Add the following code to the top of your `apps/agents-api/src/index.ts` file:

```typescript
logger = getLogger('agents-api');
logger.addTransport({
  target: 'pino-sentry-transport',
  options: {
    sentry: {
      dsn: process.env.SENTRY_DSN,
    },
  },
});
```

## Additional Resources

For more information on how to configure Sentry, you can consult the official Sentry Node.js [documentation](https://docs.sentry.io/platforms/node/).


# Inkeep Content Writer (/deployment/inkeep-enterprise/content-writer)

Configure the Inkeep Content Writer agent and trigger it from GitHub and Zendesk.



## Overview

The Inkeep Content Writer ensures that your documentation is comprehensive and up-to-date with the latest production changes.

Given a GitHub pull request or a solved Zendesk support ticket, the Content Writer will analyze your documentation for gaps and generate updates for you to review and publish.

You can also request documentation changes directly from [Slack](/talk-to-your-agents/slack/overview) by mentioning `@inkeep`.

## Customizing the Content Writer

The Inkeep team will deploy and set up the Content Writer in your Inkeep Enterprise tenant. Once set up, you can fully customize the agent to fit your needs.

Customization options include:

* [Skills](/visual-builder/skills): Reusable instructions like style guides and best practices that you can attach to sub-agents.
* Prompts: Tailored prompts that sub-agents use to coordinate documentation updates.
* [Tools](/visual-builder/tools/mcp-servers): Tools give the Content Writer access to external systems and services that it can use to research and ground its output.
* Output CMS: The CMS where the documentation updates will be published, such as GitHub, Zendesk, or Notion.
* [Triggers](/visual-builder/triggers/webhooks): The events that trigger the Content Writer.

## GitHub integration

Use the GitHub integration to trigger the Content Writer on merged pull requests or when you mention `@inkeep` in a pull request comment or review.

### Required permissions and app installation

To use Content Writer with GitHub, install the GitHub Work App and grant it access to the necessary repositories. You must be a GitHub admin to install the GitHub Work App.

The GitHub App requests the following permissions:

| Permission    | Access         | Reason                                                                   |
| ------------- | -------------- | ------------------------------------------------------------------------ |
| Contents      | Read and write | Read repository files and propose documentation changes                  |
| Pull requests | Read and write | Read PR context and create new documentation PRs                         |
| Metadata      | Read           | Identify repositories and basic GitHub resources                         |
| Members       | Read           | Resolve organization and repository membership context                   |
| Issues        | Read           | Use issue context when Content Writer is triggered from issue-linked PRs |
| Discussions   | Read           | Use discussion context when relevant to a writing request                |
| Checks        | Read and write | Report Content Writer status back to GitHub                              |

Follow the steps below to install and configure the GitHub Work App.

<Steps>
  <Step>
    ### Install the Inkeep GitHub app

    From `app.inkeep.com/<your-tenant-id>/work-apps/github`, click **Connect GitHub Organization**.

    <Image src="/images/github-connection-screen.png" alt="GitHub Work App page with connected organizations and Connect GitHub Organization button" />
  </Step>

  <Step>
    ### Add the Content Writer workflow

    Add the following workflow to each repository where you want comment-based or merge-based triggers.

    ```yaml title=".github/workflows/inkeep-content-writer.yml"
    name: Inkeep Content Writer

    on:
      pull_request:
        types: [closed]
      issue_comment:
        types: [created]
      pull_request_review_comment:
        types: [created]
      pull_request_review:
        types: [submitted]

    permissions:
      contents: read
      pull-requests: write
      id-token: write

    jobs:
      trigger-agent:
        if: |
          (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.user.login != 'inkeep[bot]') ||
          (
            (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@inkeep') &&
             (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')) ||
            (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@inkeep') &&
             (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')) ||
            (github.event_name == 'pull_request_review' && contains(github.event.review.body || '', '@inkeep') &&
             (github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'MEMBER' || github.event.review.author_association == 'COLLABORATOR'))
          )
        runs-on: ubuntu-latest
        steps:
          - name: Trigger Inkeep Agent
            uses: inkeep/inkeep-agents-action@fbbc92842bd025f044cb7e4d827882fd85914f26
            with:
              trigger-url: ${{ secrets.INKEEP_CONTENT_WRITER_TRIGGER_URL }}
              # Filters out PRs that don't match the given regex. Update regex as desired or remove entirely
              pr-title-regex: '^(feat|fix|docs|chore)(\(.+\))?:'
    ```
  </Step>

  <Step>
    ### Store the trigger URL

    Add the Content Writer trigger URL to your repository secrets as `INKEEP_CONTENT_WRITER_TRIGGER_URL`.
    The trigger URL can be found at `https://app.inkeep.com/<your-tenant-id>/projects/content-writer/triggers?tab=webhooks`.
  </Step>
</Steps>

### Triggering the Content Writer from GitHub

The workflow triggers the Content Writer in the following cases:

* **Mention `@inkeep` in a pull request comment, review comment, or review body** to provide feedback to the Content Writer.
* **Merge a pull request** to trigger post-merge content follow-up.

The Inkeep team can help you customize the GitHub workflow to fit your needs.

## Zendesk integration

Use the Zendesk integration to trigger the Content Writer from support tickets.

### Create a Zendesk role

Create a custom Zendesk role for the service account that the Content Writer will use.

<Steps>
  <Step>
    ### Open Roles

    In Zendesk Admin Center, go to **People** → **Roles**.
  </Step>

  <Step>
    ### Create the role

    Click **Create role**.
  </Step>

  <Step>
    ### Select permissions

    Select these Support permissions for the Content Writer service account role:

    | Permission                            | Setting                                      |
    | ------------------------------------- | -------------------------------------------- |
    | **Tickets they can access**           | Within their groups                          |
    | **Groups they can assign tickets to** | Assign tickets to any group                  |
    | **Brands they can assign tickets to** | Only assign tickets to brands they belong to |
    | **Manage suspended tickets**          | Not allowed                                  |
    | **Edit ticket properties**            | Disabled                                     |
    | **Commenting permissions**            | Private comments only                        |
  </Step>
</Steps>

### Create a Zendesk service account

Create a Zendesk team member that Content Writer will use to access ticket data.

<Steps>
  <Step>
    ### Open Team members

    In Zendesk Admin Center, go to **People** → **Team members**.
  </Step>

  <Step>
    ### Add a team member

    Click **Add team member**.
  </Step>

  <Step>
    ### Create the service account user

    Create a new user with a shared email address, such as `support-bot@yourcompany.com`.
  </Step>

  <Step>
    ### Assign the custom role

    Set the Support role to the custom service account role you created.
  </Step>
</Steps>

### Add the service account to groups

Add the Zendesk service account to the groups that contain the tickets Content Writer should be able to read.

<Steps>
  <Step>
    ### Open Groups

    In Zendesk Admin Center, go to **People** → **Groups**.
  </Step>

  <Step>
    ### Add the service account

    Add the service account to each group that Content Writer should access.
  </Step>
</Steps>

### Create an API token

Create an API token for the Zendesk service account.

<Steps>
  <Step>
    ### Open API tokens

    In Zendesk Admin Center, go to **Apps and integrations** → **APIs** → **API tokens**.
  </Step>

  <Step>
    ### Add the token

    Click **Add API token** and name it something like `Inkeep Content Writer Integration`.
  </Step>

  <Step>
    ### Save the token

    Click **Save**, then copy and store the token. Zendesk will not show the full token again after you leave the page.
  </Step>
</Steps>

### Connect to the Zendesk MCP

Connect the Zendesk service account token to the Zendesk MCP server in Inkeep.

<Steps>
  <Step>
    ### Open your Content Writer project

    Log in to [Inkeep Enterprise](https://app.inkeep.com) and open your Content Writer project.
  </Step>

  <Step>
    ### Create a bearer credential

    In the left-hand menu, click **Credentials** → **New** → **Bearer Authentication**.

    <Image src="/images/new-credential-screen.png" alt="New credential page with Browse providers and Bearer authentication options" />
  </Step>

  <Step>
    ### Fill in credential details

    Use these values:

    | Field       | Value                                                            |
    | ----------- | ---------------------------------------------------------------- |
    | **Name**    | A credential name, such as `Zendesk Content Writer Integration`  |
    | **API Key** | The API token for the Zendesk service account created previously |
  </Step>

  <Step>
    ### Add Zendesk headers

    Under **Headers**, add:

    | Header                | Value                                                                     |
    | --------------------- | ------------------------------------------------------------------------- |
    | `x-zendesk-email`     | The service account email address                                         |
    | `x-zendesk-subdomain` | Your Zendesk subdomain. For `yourcompany.zendesk.com`, use `yourcompany`. |
  </Step>

  <Step>
    ### Link the credential to the MCP server

    Check **Link this credential to an MCP server**, then select the Zendesk MCP server from the dropdown.
  </Step>
</Steps>

### Create the Zendesk webhook trigger

Create a Zendesk webhook that sends ticket updates to the Content Writer.

<Steps>
  <Step>
    ### Open Webhooks in Zendesk

    In Zendesk Admin Center, go to **Apps and integrations** → **Webhooks**. You can also search for **Webhooks** from Admin Center.
  </Step>

  <Step>
    ### Create a webhook

    Click **Create webhook**. Under **Select a way to connect**, choose **Trigger or automation**.
  </Step>

  <Step>
    ### Configure the webhook

    Use these settings:

    | Field              | Value                                                                                                                                                                                                        |
    | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | **Endpoint URL**   | Your Content Writer webhook URL. Find it at `https://app.inkeep.com/<your-tenant-id>/projects/content-writer/triggers?tab=webhooks`, or contact your Inkeep account team if it has not been provisioned yet. |
    | **Request method** | `POST`                                                                                                                                                                                                       |
    | **Request format** | `JSON`                                                                                                                                                                                                       |
    | **Authentication** | `None`                                                                                                                                                                                                       |
  </Step>

  <Step>
    ### Create the webhook

    Click **Create webhook**.
  </Step>

  <Step>
    ### Share the webhook secret

    Open the new webhook, find **Secret key**, click **Reveal secret**, and send it to the Inkeep team through your shared secure credential store.
  </Step>
</Steps>

### Customize the Zendesk trigger

In the following example, the trigger is configured to send solved tickets with the `inkeep_content_writer` tag to the Content Writer. The Inkeep team can help you customize the trigger to fit your needs.

<Steps>
  <Step>
    ### Set the category

    Set **Trigger Category** to **Notifications**.
  </Step>

  <Step>
    ### Add conditions

    Under **Conditions**, configure the trigger to meet **ALL** of these conditions:

    * **Ticket** → **Tags** | **Contains at least one of the following** | `inkeep_content_writer`
    * **Ticket** → **Status category** | **Is** | **Solved**

    This trigger runs when a ticket is solved and the `inkeep_content_writer` tag is present.
  </Step>

  <Step>
    ### Add the webhook action

    Under **Actions**, add **Notify by** → **Active webhook**, then select the webhook you created.
  </Step>

  <Step>
    ### Add the payload

    In the webhook payload body, paste:

    ```json
    {"ticket_id":"{{ticket.id}}"}
    ```
  </Step>

  <Step>
    ### Enable the trigger

    Click **Create** to enable the trigger.
  </Step>
</Steps>


# Get Started with Inkeep Enterprise (/deployment/inkeep-enterprise/get-started)

Get started with Inkeep Enterprise



## Overview

With Inkeep Enterprise, we manage the infrastructure for you so you can focus on building your agents. In this guide, will walk you through the steps to build your first agent in the Inkeep Enterprise Visual Builder.

### Prerequisites

* An Inkeep Enterprise account - if you don't have one, you can sign up for the [demo](https://inkeep.com/demo?cta_id=docs_nav)

### Step 1: Login using your credentials

<img src="/images/inkeep-cloud-login.png" alt="Inkeep Cloud Enterprise Login" width="600" style={{ borderRadius: '8px' }} />

### Step 2: Enter a project

Once you're logged in, you'll be redirected to the Inkeep Enterprise project overview page. If you are part of a team, you might see a list of existing projects like this:

<img src="/images/inkeep-cloud-project-overview.png" alt="Inkeep Cloud Project Overview" style={{ borderRadius: '8px' }} />

If you are part of a team, click into the project your team has created. If you are not part of a team, you can create a new project by clicking the "Create Project" button.

### Step 3: Inspect the project

Once you're in the project, you'll see the project overview page. You can see the list of agents that are part of the project.

<img src="/images/inkeep-cloud-agent-overview.png" alt="Inkeep Cloud Agent Overview" style={{ borderRadius: '8px' }} />

In the MCP Servers section, you can see the list of MCP servers that are part of the project:

<img src="/images/inkeep-cloud-mcp-overview.png" alt="Inkeep Cloud MCP Servers" style={{ borderRadius: '8px' }} />

<Tip title="MCP Servers">
  MCP servers are the tools that your agents can use to interact with external APIs and services.
</Tip>

Notice the Inkeep Enterprise Search MCP server, part of [Inkeep's Enterprise offering](https://inkeep.com/enterprise). It allows your agent to connect to 25+ data sources to create a unified knowledge base that your agents can access.

### Step 4: Build your agent

Back in the agent overview page, click the "Create Agent" button to create a new agent. Give your agent a name, let's call it "Knowledge agent".

<video src="/videos/inkeep-cloud-create-knowledge-agent.mp4" controls style={{ borderRadius: '10px', width: '100%' }} />

Inkeep will prompt you to build your agent using natural language. Let's ask Inkeep to build a knowledge agent that can answer questions about your knowledge base:

<img src="/images/inkeep-cloud-chat-to-edit.png" alt="Inkeep Cloud Create Agent" style={{ borderRadius: '8px' }} />

### Step 5: Try the agent

Once Inkeep has built your agent, try it out by clicking the "Try it" button.

<video src="/videos/inkeep-cloud-try-it.mp4" controls style={{ borderRadius: '10px', width: '100%' }} />

### Step 6: Edit the agent

You can edit your agent by dragging and dropping new sub agents and tools onto the canvas. Here, we'll extend our knowledge agent to use a web search agent if it needs to find more information.

<video src="/videos/extend-kb.mp4" controls style={{ borderRadius: '10px', width: '100%' }} />

### Next Steps

<Cards>
  <Card title="Build a Meeting Prep Agent" icon="LuSparkles" href="https://inkeep.com/visual-guide">
    Learn how to build a meeting prep agent that can help you prepare for meetings by researching the company and creating a meeting outline.
  </Card>
</Cards>


# Agent Engineering Best Practices (/guides/agent-engineering)

Design AI agents that are focused, safe, and reliable. Learn the principles, patterns, and techniques for building high-quality agent systems.



Agent engineering is the discipline of designing AI agents that reliably accomplish their goals. This section covers the full lifecycle of agent design — from writing effective prompts, to coordinating specialists, to troubleshooting and iterating on prompts.

## Who this is for

These best practices apply to anyone designing AI agents with Inkeep — whether you're building a single-purpose sub agent or a multi-agent system.

## Section overview

<Cards>
  <Card title="Prompt Structure" icon="LuFileText" href="/guides/agent-engineering/prompt-structure">
    Write high-quality sub agent prompts
  </Card>

  <Card title="Multiagent Workflows" icon="LuWorkflow" href="/guides/agent-engineering/multiagent-workflows">
    Design coordinator sub agents that delegate to specialists
  </Card>

  <Card title="Tuning Agents" icon="LuGauge" href="/guides/agent-engineering/tuning-agents">
    Balance cost, latency, and quality with model, reasoning, caching, and limit knobs
  </Card>

  <Card title="Troubleshooting" icon="LuShieldAlert" href="/guides/agent-engineering/troubleshooting">
    Diagnose common issues with agents and apply targeted fixes
  </Card>
</Cards>


# Designing Multiagent Workflows (/guides/agent-engineering/multiagent-workflows)

Design multiagent workflows with coordinator sub agents that delegate to specialists, aggregate results and drive the overall process forward.



A multi-agent workflow uses a **coordinator sub agent** that delegates subtasks to specialized sub agents, aggregates their results, and drives the overall process forward.

## Single vs multi-agent

The first design decision is whether your Agent needs one sub agent or several.

A **single sub agent** is usually enough when:

* The task is well-scoped and stays in one domain
* Required tools are closely related
* The conversation does not need to switch between specialist domains

A **multi-agent workflow** is worth it when:

* Different phases require different expertise, prompts, or tools
* A coordinator must combine outputs from several specialists
* You need parallel specialist work and explicit quality gates

<Tip>
  If a sub-agent needs more than five tools, split it into smaller, specialized sub-agents to avoid context overload.
</Tip>

## Sequential vs parallel delegation

When your coordinator delegates work, you usually choose between two patterns:
**sequential** (one step at a time) or **parallel** (multiple steps at once).
Pick based on whether tasks depend on each other.

Use **sequential delegation** when:

* Later tasks need outputs from earlier tasks
* You want clear phases (plan -> implement -> verify)
* A mistake early on would affect everything that follows

Use **parallel delegation** when:

* Tasks are independent and can start from the same input
* You want faster overall turnaround
* Different specialists can review the same request from different angles

<Tip>
  Quick rule: if one task changes what the next task should do, use sequential delegation. If tasks can all start with the same context and only need to be combined at the end, use parallel delegation.
</Tip>

## Transfers vs delegation relationships

Define the transfer and delegation relationships between the coordinator and the specialized sub agents. See [Sub Agent Relationships](/typescript-sdk/agent-relationships) section for more details.

## Designing the coordinator prompt

The coordinator sub agent is the agent which will delegate tasks to the specialized sub agents. Its prompt should describe the overall workflow: what phases to run, when to delegate, and how to aggregate results. See section on [workflow checklist](/guides/agent-engineering/prompt-structure#4-workflow-checklist) for more details.


# Writing High-Quality Prompts (/guides/agent-engineering/prompt-structure)

A complete guide to writing sub agent prompts: structure, operating principles, role and personality statements, output formats, and escalation rules.



A well-structured prompt is the foundation of reliable agent behavior. This page covers the recommended sections for a sub agent prompt and how to write each one effectively.

<Callout type="tip" title="Use the Agent Prompt for shared instructions">
  The **Agent Prompt** is inherited by all sub-agents in an agent and is often underutilized. Use it for instructions you don't want to repeat across every sub-agent: MCP tool usage guidelines, global constraints, output conventions, and other rules that apply universally. Reserve sub-agent prompts for role-specific behavior.
</Callout>

Structure your sub-agent prompt in this order:

1. **Role and mission** — who the agent is and what it optimizes for
2. **Scope and non-goals** — prevents accidental overreach
3. **Operating principles** — directness vs suggestions, decision frameworks
4. **Workflow checklist** — steps the agent can follow
5. **Tool-use policy** — what tools to use and when
6. **Output format** — headings, verbosity limits, evidence expectations
7. **Uncertainty and escalation** — when to ask, when to proceed, when to defer
8. **Few-shot examples (optional)** — examples that demonstrate the expected behavior and output quality

## 1. Role and mission

The role and mission sets the sub agent's identity and paradigm in 2-4 sentences. It should:

* Declare what **excellence looks like** for this role
* Describe concrete behaviors the **best humans** in this role would exhibit

Write in second person ("You are...", "Do..."). Avoid first-person commitments ("I will...") unless the agent is expected to do so.

Template:

```markdown
You are [role identity] who [what excellence looks like in practice].

You [concrete positive behavior].
You [concrete positive behavior].
You [concrete positive behavior].
```

Example:

```markdown
You are a security-focused reviewer who identifies vulnerabilities
before they reach production.

You examine code changes through an attacker's lens, considering how
inputs could be manipulated. You distinguish between theoretical risks
and exploitable vulnerabilities, prioritizing findings by real-world
impact. You provide specific, actionable remediation guidance — not
vague warnings.
```

## 2. Scope and non-goals

Use scope and non-goals to prevent the agent from accidentally overreaching. Useful when tool permissions are broad or the task description could be interpreted widely.

Example:

```markdown
## Scope
- Triage incoming customer support tickets about billing and subscriptions
- Ask clarifying questions to gather missing account or invoice details
- Classify issue severity and route tickets to the correct support queue

## Non-goals
- Handling legal disputes, chargebacks, or fraud investigations
- Answering product roadmap or engineering implementation questions
- Creating new policies that are not documented in the support playbook
```

## 3. Operating principles

Use a mix of direct requirements and suggestions:

* **Direct requirements** (must/never) for correctness and safety
* **Suggestions** (prefer/consider) where multiple strategies can work

```markdown
## Operating principles
- Clarify before proceeding: if instructions are ambiguous, surface
  what's unclear rather than filling gaps with assumptions
- Weigh sources appropriately: official docs and established patterns
  take precedence over examples or inferred conventions
- Model downstream effects: before making a change, consider what it
  could break and what constraints it might conflict with
- Match confidence to certainty: if multiple valid approaches exist,
  present them with tradeoffs rather than silently picking one
```

Guidance:

* Prefer "Do X" over "It's good to X"
* Provide a default path and an escape hatch
* Avoid long background explanations unless they change behavior

## 4. Workflow checklist

Use a checklist to guide the agent's behavior. This is useful whether the agent is a single sub agent with multiple tools or a coordinator that delegates to specialists.

Example:

```markdown
## Workflow
- [ ] Use the `ticket-intake` tool first to capture the customer issue, urgency, and required fields
- [ ] Use the `account-lookup` tool next to verify subscription status, invoice history, and payment state
- [ ] If specialist handling is needed, delegate to the `billing-escalation-agent` with a structured handoff (goal, evidence, constraints)
- [ ] Synthesize tool outputs and the specialist result into a policy-approved customer response
```

## 5. Tool-use policy

Make tool usage explicit — which tools to prefer, in what order, and what to avoid:

```markdown
## Tool policy
- Use the knowledge base tool before making assessments
- For data lookups, use the search tool with specific queries
- Report only relevant results — don't dump full tool outputs
- Do not execute code from untrusted sources
```

## 6. Output format

The output format is the most important section for multi-agent integration. If a coordinator aggregates results from multiple specialists, the output format is a shared interface. Define it precisely:

* **Exact headings** the output must include
* **Severity levels** or categories for findings
* **Evidence expectations** (line numbers, file paths, short excerpts)
* **Verbosity bounds** (e.g., "max 1-2 screens unless asked for more")

```markdown
## Output format

Return findings in this format:

### TL;DR (2-5 bullets)

### Findings (prioritized)
For each finding:
- **Severity**: CRITICAL | HIGH | MEDIUM | LOW
- **File**: path/to/file.ts:line
- **Issue**: one-sentence description
- **Evidence**: relevant code excerpt (keep short)
- **Recommendation**: specific fix

### Recommended next actions
1. [highest priority action]
2. [second priority action]

### Open questions
- [only list what materially affects next steps]
```

## 7. Uncertainty and escalation

Define when the agent should ask for help vs proceed with assumptions:

```markdown
## Escalation rules
- **Ask** when: requirements are ambiguous and the choice materially
  affects the outcome
- **Proceed with assumptions** when: the decision is low-stakes and
  reversible; label assumptions explicitly
- **Return partial results** when: blocked on external dependency or
  missing information; include what you have and what's needed
```

## 8. Few-shot examples (optional)

* 2-3 well-chosen examples outperform more
* Order matters: place the most representative example last (recency effect)
* One weak example degrades all examples — curate carefully


# Troubleshooting (/guides/agent-engineering/troubleshooting)

Fix common issues with sub agents.



If your sub agent gives weak or inconsistent results, you usually do not need a full prompt rewrite. Start by matching symptoms, apply one targeted prompt fix, and testing again.

## Quick symptom picker

| If you see this                                 | Start with                                                    |
| ----------------------------------------------- | ------------------------------------------------------------- |
| Silent assumptions or over-questioning          | [Ambiguity and assumptions](#ambiguity-and-assumptions)       |
| Conflicting sources handled poorly              | [Evidence and source quality](#evidence-and-source-quality)   |
| No escalation when blocked                      | [Decision boundaries and risk](#decision-boundaries-and-risk) |
| Long, repetitive responses with buried findings | [Output clarity and structure](#output-clarity-and-structure) |
| Only some list items processed                  | [Deterministic operations](#deterministic-operations)         |
| Date filtering is inaccurate                    | [Deterministic operations](#deterministic-operations)         |

## Copy-paste guardrails by category

Use these snippets as starting points.

<Tabs>
  <Tab title="Ambiguity">
    ```markdown
    - If requirements are ambiguous and the decision is high impact, ask before proceeding.
    - For low-risk, reversible decisions, proceed with labeled assumptions.
    - If assumptions materially affect outcomes, list them explicitly.
    - If new input conflicts with prior constraints, surface the conflict before proceeding.
    ```
  </Tab>

  <Tab title="Evidence">
    ```markdown
    - When sources conflict, state the conflict and provide options with tradeoffs.
    - Separate direct evidence from inference.
    - Prioritize sources: official docs -> established repo patterns -> examples -> blog posts.
    - Call out when applying guidance outside its original context.
    - If confidence is low, say "I don't know" and state what must be verified.
    ```
  </Tab>

  <Tab title="Boundaries">
    ```markdown
    - Before acting, check: what could this break and who depends on it?
    - Escalate when requests are out of scope, low-confidence/high-impact, or blocked by permissions.
    - Define directive strength: must (non-negotiable), should (strong default), consider (optional).
    - When multiple valid paths exist, present options with tradeoffs.
    ```
  </Tab>

  <Tab title="Output shape">
    ```markdown
    - Return output in a strict format with ordered headings.
    - Lead with 2-5 key bullets, then details.
    - Avoid long preambles and repeated phrasing.
    ```
  </Tab>

  <Tab title="Deterministic ops">
    ```markdown
    - Do not rely on the LLM to iterate through lists; use programmatic loops.
    - When processing N items, call the sub agent N times from a parent tool.
    - Validate that all items in a list were processed before returning results.
    - Handle date/time filtering in the tool, not in the prompt; pass date parameters to the tool.
    ```
  </Tab>
</Tabs>

## Common issues and fixes

### Ambiguity and assumptions

<Accordions>
  <Accordion title="The agent assumes missing details and proceeds">
    **What you see:** Silent assumptions, fabricated defaults, no clarification.

    **Why it happens:** The prompt says what to do, but not when to pause and ask.

    **Fix:**

    * Add: "If requirements are ambiguous and the decision is high impact, ask before proceeding."
    * Add: "If you proceed with assumptions, state them explicitly."
  </Accordion>

  <Accordion title="The agent asks too many clarifying questions">
    **What you see:** Repeated low-stakes questions and slow progress on reversible decisions.

    **Why it happens:** The prompt does not define when asking is required versus optional.

    **Fix:**

    * Add ask policy: ask only for high-impact ambiguity or preference-sensitive decisions.
    * Add proceed policy: for low-risk decisions, proceed with labeled assumptions.
  </Accordion>

  <Accordion title="The agent overreacts to the most recent message">
    **What you see:** New feedback causes the agent to ignore earlier goals and constraints.

    **Why it happens:** The prompt does not include continuity guidance.

    **Fix:**

    * Add: "New input adds context; it does not erase prior requirements."
    * Add: "If new input conflicts with prior constraints, surface the conflict before proceeding."
  </Accordion>

  <Accordion title="Assumptions compound without visibility">
    **What you see:** Output depends on multiple hidden assumptions that were never validated.

    **Why it happens:** The prompt does not require explicit assumption disclosure.

    **Fix:**

    * Add: "When assumptions affect outcome, list them explicitly before final recommendations."
    * Add: "If assumptions stack, pause and request confirmation before further work."
  </Accordion>
</Accordions>

### Evidence and source quality

<Accordions>
  <Accordion title="The agent flattens nuance across conflicting sources">
    **What you see:** Conflicting sources are treated as agreement; alternatives are not named.

    **Why it happens:** The prompt does not explain how to handle ambiguous or conflicting evidence.

    **Fix:**

    * Add: "When sources conflict, state the conflict and provide options with tradeoffs."
    * Add: "Separate direct evidence from inference."
  </Accordion>

  <Accordion title="The agent treats weak sources as equally authoritative">
    **What you see:** Low-trust examples are used instead of official docs or established repo patterns.

    **Why it happens:** The prompt does not define source priority.

    **Fix:**

    * Add source priority guidance, for example: official docs -> established repo patterns -> examples -> blog posts.
    * Add: "Call out when applying guidance outside its original context."
  </Accordion>

  <Accordion title="The agent invents details when uncertain">
    **What you see:** Plausible but unverified claims are presented confidently.

    **Why it happens:** The prompt does not allow uncertainty language, so the agent guesses.

    **Fix:**

    * Add: "'I don't know' is valid when confidence is low."
    * Add: "Do not invent missing details; state what must be verified."
  </Accordion>
</Accordions>

### Decision boundaries and risk

<Accordions>
  <Accordion title="The agent never escalates">
    **What you see:** The agent tries to answer everything, including out-of-scope decisions.

    **Why it happens:** The prompt does not define escalation boundaries.

    **Fix:**

    * Add explicit escalation criteria: out-of-scope requests, low confidence on high-impact decisions, missing permissions.
    * Add expected escalation output: what is blocked, why, and what is needed to continue.
  </Accordion>

  <Accordion title="The agent misreads directive strength">
    **What you see:** Suggestions are treated as requirements, or hard requirements are ignored.

    **Why it happens:** Directive strength is not defined consistently.

    **Fix:**

    * Define terms once: `must` = non-negotiable, `should` = strong default, `consider` = optional.
    * Rewrite ambiguous constraints to use one of those terms consistently.
  </Accordion>

  <Accordion title="The agent presents one option only">
    **What you see:** One option is presented even when multiple valid options exist.

    **Why it happens:** The prompt does not require tradeoff framing when uncertainty is high.

    **Fix:**

    * Add: "When multiple valid options exist, present options with tradeoffs."
  </Accordion>
</Accordions>

### Output clarity and structure

<Accordions>
  <Accordion title="The agent is verbose, repetitive, or buries key points">
    **What you see:** Long preambles, repeated phrasing, and key findings that are hard to locate.

    **Why it happens:** The prompt does not define a clear output shape.

    **Fix:**

    * Add a strict output format with ordered headings.
    * Add verbosity limits, for example: "Lead with 2-5 key bullets, then details."
  </Accordion>
</Accordions>

### Deterministic operations

<Accordions>
  <Accordion title="The agent only processes some items in a list">
    **What you see:** You give the agent a list of 10 companies and ask it to call a tool or sub agent for each one, but only 3-5 get processed.

    **Why it happens:** LLMs are not reliable iterators. When asked to loop through a list and perform an action for each item, they often stop early, skip items, or lose track of progress. This is a fundamental limitation of how LLMs process lists.

    **Fix:**

    * Do not rely on the LLM to iterate. Instead, use programmatic iteration in a [function tool](/typescript-sdk/tools/function-tools) from a separate agent which calls the sub agent using a [webhook](/typescript-sdk/triggers/webhooks).
    * The function tool accepts the list, loops through each item in code, and calls the sub agent via webhook once per item.
    * The calling agent calls the function tool once with the full list; the tool handles the iteration deterministically.
  </Accordion>

  <Accordion title="The agent filters dates incorrectly">
    **What you see:** You ask for "messages from the past 24 hours" but the agent returns messages from days ago, or misses recent ones. Date comparisons are inconsistent.

    **Why it happens:** LLMs struggle with date/time reasoning. They may misparse timestamps, miscalculate relative dates ("past 24 hours"), or inconsistently compare date formats. This is especially problematic when filtering large result sets.

    **Fix:**

    * Move date filtering into the tool itself. Add date parameters to the tool schema.
    * Have the tool compute the date range and filter results before returning them to the agent.
    * The agent's job is to extract the user's intent ("past 24 hours") and pass it as a parameter; the tool does the actual filtering.
  </Accordion>
</Accordions>

## Adding fixes to your prompt

You can add these guardrails in three ways. If you are new to prompt writing, start with Option A.

### Option A: Dedicated troubleshooting section

```markdown
## Prompt pitfalls to avoid

- If requirements are unclear on a high-impact decision, ask before proceeding.
- When sources conflict, acknowledge the conflict and separate evidence from inference.
- Lead with key findings and avoid repeated wording.
```

### Option B: Weave fixes into operating principles

```markdown
## Operating principles

- Clarify before action on high-impact ambiguity.
- Weigh evidence by source authority and relevance.
- Match confidence to certainty and provide options when needed.
```

### Option C: Hybrid

Use operating principles for defaults, then add a short "watch-outs" block near the end of the prompt.


# Tuning Agents for Cost and Performance (/guides/agent-engineering/tuning-agents)

The knobs Inkeep gives you to balance cost, latency, quality, and reliability — model choice per slot, reasoning effort, prompt caching, output contracts, description hygiene, and execution limits.



Once an agent works, the next job is tuning it. Inkeep gives you a set of mostly-independent knobs to trade off **cost, latency, quality, and reliability** — without rewriting prompts. This guide explains each knob and when to reach for it. Each section links to its reference page.

## Pick the right model for each slot

Every agent has three model slots, and they don't all need the same model:

| Slot               | Does                                                       | Reach for                                                                                                  |
| ------------------ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `base`             | Text generation and reasoning — the hard work              | The most capable model the task needs                                                                      |
| `structuredOutput` | JSON / structured output only                              | A smaller, cheaper model that reliably emits valid JSON (for example `claude-haiku-4-5` or `gpt-4.1-mini`) |
| `summarizer`       | Summaries and status updates — high-volume, low-difficulty | The cheapest fast model (for example `gpt-5.4-nano` or `gemini-2.5-flash-lite`)                            |

`structuredOutput` and `summarizer` both fall back to `base` when unset. The win is overriding them: the summarizer in particular fires often and rarely needs a frontier model, so pointing it at a small model cuts cost with no quality loss on the main answer.

<Tip>
  The 

  `inkeep init`

   defaults are a safe starting point, not the optimum — see the 

  [CLI defaults table](/typescript-sdk/models#cli-defaults)

  . Tune the cheap slots first.
</Tip>

See [Model Configuration](/typescript-sdk/models) for the full model list and how to set each slot.

## Control reasoning

Extended reasoning (also called "thinking") helps on genuinely hard, multi-step problems — but it adds latency and token cost on every call. **Conversational, routing, and simple-lookup agents usually don't need it.**

Defaults differ by provider, so know what you're starting from:

| Provider                   | Reasoning by default                  | Turn it down / off with                                                                            |
| -------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Anthropic** (Claude 4.x) | Off — nothing thinks until you opt in | Simply omit the `thinking` block                                                                   |
| **OpenAI** (GPT-5.x)       | Mostly off                            | `providerOptions.openai.reasoningEffort: 'none'`                                                   |
| **Google** (Gemini)        | Often **on** by default               | `providerOptions.google.thinkingConfig: { thinkingBudget: 0 }` on 2.5 Flash, or use 2.5 Flash-Lite |

<Tip>
  For a simple conversational or routing sub agent, leave reasoning off and reserve it for the sub agents doing real planning. On Google models especially, check whether you're paying for thinking you don't need.
</Tip>

Providers change their defaults over time — confirm against the provider's current docs. See [Provider Options](/typescript-sdk/models#provider-options) for the exact syntax per provider.

## Anthropic prompt caching is on by default

Inkeep enables Anthropic prompt caching automatically, on the main agent generation call. The stable prompt prefix (tool definitions plus the system prompt) is reused across turns at the provider's reduced cache-read rate, which cuts cost for most agentic workflows where a sub agent takes several steps (multiple tool calls in a turn). Other call sites (status updates, distillation) get only whatever caching the provider applies automatically. **No configuration is required.**

<Note>
  If you ever need to confirm: prompt caching is enabled by default — you don't need to turn it on. See [Prompt Caching](/guides/observability/prompt-caching) to observe cache hits and to control it.
</Note>

## Constrain responses with output contracts

In a multi-stage pipeline (for example `query → select → respond`), the intermediate stages shouldn't narrate to the user. An **output contract** lets you require structured output, specific components or artifacts, or a transfer — and forbid free text — instead of relying on prompt discipline. That saves the cost of wasted narration and makes the pipeline more reliable.

Configure it in code via a sub agent's `outputContract`, or visually in the **Guardrails** section of a sub agent.

* [Output Contract (TypeScript SDK)](/typescript-sdk/structured-outputs/output-contract)
* [Guardrails (Visual Builder)](/visual-builder/structured-outputs/output-contract)

## Keep tool and skill descriptions tight

Tool descriptions, always-loaded skills, and the outline of on-demand skills all sit in the system prompt on **every turn**. Verbose descriptions inflate input cost and can dilute the model's routing decisions. Make each description specific and concise, and front-load the "use when…" trigger so the agent can route on the first words.

<Tip>
  Skill descriptions are capped at 

  **1024 characters**

   — but the practical target is much shorter. A tight, trigger-first description routes better than a long one. See 

  [Writing effective descriptions](/visual-builder/skills#writing-effective-descriptions)

  .
</Tip>

See [Skills](/typescript-sdk/skills) and [MCP Servers](/guides/mcp-servers/overview) for where descriptions are authored.

## Bound runaway execution

Caps stop a misbehaving agent from running up cost in a loop. Two matter most:

* **Generation steps per sub-agent turn** (`stopWhen: { stepCountIs }`, default 12) — how many LLM calls a sub agent may make in one activation.
* **Transfer count** (`stopWhen: { transferCountIs }`, default 10) — how many hand-offs can happen in a turn.

Raise these for long-running orchestration agents; keep them low for tight, predictable agents. See [Configure Runtime Limits](/typescript-sdk/configure-runtime-limits).


# Build an AI SDR chatbot without code (/guides/agents/ai-sdr-chatbot)

Step-by-step tutorial to build a public chatbot that answers product and pricing questions from your knowledge base, then books a meeting in-chat the moment a visitor shows sales intent.



## Overview

In this tutorial, you'll build a public-facing AI SDR chatbot in the Inkeep Visual Builder. It does two jobs in a single conversation — no handoff:

1. Answers product and pricing questions grounded in your knowledge base, with citations.
2. When a visitor shows sales intent — pricing, a demo, "talk to sales" — it shows a booking button that opens a Cal.com calendar so they can schedule a meeting without leaving the chat.

After they book, the agent confirms the meeting in-chat and keeps answering questions. One agent meets the visitor at the speed they expect: instant answers, and a booked meeting while the intent is still warm.

The agent logic is configured entirely in the Visual Builder (no code). The in-chat booking experience requires a small React component (\~60 lines) to render the Cal.com calendar.

<Note>
  **You'll need Inkeep to run this.** The agent is built in the Inkeep Visual Builder, and the in-chat experience — the chat thread, citations, and the booking calendar — is rendered by the Inkeep embedded chat UI. Both require an Inkeep account. [Get Inkeep →](https://inkeep.com/demo)
</Note>

## Prerequisites

* An Inkeep account with access to the Visual Builder and embedded chat UI — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have one yet.
* A docs-grounded assistant built from the [Customer Assistant setup](/guides/agents/customer-assistant/setup) and [Citations](/guides/agents/customer-assistant/citations) guides — a sub agent connected to Inkeep Unified Search that answers with citations. That's the Q\&A foundation this tutorial extends.
* A [Cal.com](https://cal.com) account with a bookable event type (e.g. `your-name/30min`).
* A React app where you'll embed the chat widget, with the Cal.com embed installed: `npm install @calcom/embed-react`. See [App Credentials](/talk-to-your-agents/app-credentials) for your App ID.

## Create the sales-booking skill

A [skill](/visual-builder/skills) is a reusable instruction block you attach to a sub agent. Putting the booking SOP in a skill — rather than the agent prompt — keeps the trigger list and the acknowledge-then-book flow in one place. When you later add a second intent path, you drop in another skill instead of rewriting the prompt.

<Steps>
  <Step>
    Go to the **Skills** tab in the left sidebar, then click **Create skill**.
  </Step>

  <Step>
    Fill in:

    * **Name**: `book-meeting-with-sales`
    * **Description**: Paste the description below — the agent uses it to decide when the skill applies.
    * **Content**: Paste the body below.
  </Step>

  <Step>
    Click **Save**.
  </Step>
</Steps>

**Description:**

```text
Use when the visitor shows sales intent — pricing, quotes, demos, trials, talking to a rep, buying, or "set up a call / next steps". Renders the SalesScheduler so they can book without leaving chat. Not for general product Q&A.
```

**Content:**

```text
# Book a meeting with sales

When the visitor signals they want to talk to a human, render an in-chat scheduler so they can book in seconds.

## Trigger on
- Pricing — "how much", "pricing", "quote", "cost"
- Sales / demo — "talk to sales", "see a demo", "get a trial"
- Buying / next steps — "purchasing", "licensing", "set up a call"

Just exploring how things work? Don't trigger — keep answering from the docs.

## What to do
1. Acknowledge in one plain-text sentence, customized to what they asked (pricing -> "Happy to connect you with our team — they can walk through pricing.").
2. Render the SalesScheduler component. No input needed.
3. After they book, confirm the date and time they mention, note the calendar invite is sent automatically, and offer to keep answering questions.

## Rules
- The acknowledgement and the scheduler must appear in the SAME response.
- Acknowledgement text comes BEFORE the component, never inside it.
- Never list time slots as text — the calendar handles selection.
- Never invent meeting times or invite details.
```

<Tip>
  Swap the trigger examples for your own product's language. Keep the body shorter than your instinct says — the agent doesn't need ceremony, and an always-loaded skill sits in the prompt on every turn.
</Tip>

## Create the scheduler data component

A [data component](/visual-builder/structured-outputs/data-components) lets the agent emit a structured UI block — here, the booking calendar — that your app renders. You'll define the schema now and wire up the React renderer later.

<Steps>
  <Step>
    Go to the **Data Components** tab in the left sidebar, then select **New data component**.
  </Step>

  <Step>
    Fill in:

    * **Name**: `SalesScheduler` *(must match the key you'll register in your chat widget)*
    * **Description**: `Renders a booking button that opens a Cal.com modal so the user can schedule a meeting. Just render it — the calendar is pre-configured.`
  </Step>

  <Step>
    Paste the schema below into the **Props schema** field, then click **Save**.
  </Step>
</Steps>

**Props schema:**

```json
{
  "type": "object",
  "properties": {
    "calLink": {
      "type": "string",
      "description": "Cal.com booking path, e.g. \"your-name/30min\". If omitted, the component falls back to the placeholder you hardcode in the renderer — replace it with your real Cal.com path."
    },
    "title": {
      "type": "string",
      "description": "Title shown above the calendar"
    }
  }
}
```

## Teach the agent when to book

Your sub agent already answers from the knowledge base with citations. Now give it the one override that makes the booking flow fire: on a sales-intent turn, skip docs Q\&A and surface the scheduler instead.

<Steps>
  <Step>
    Open your agent canvas and click the sub agent to open its config.
  </Step>

  <Step>
    Replace the **Prompt** with the version below.
  </Step>

  <Step>
    Scroll to the **Skills** picker and add `book-meeting-with-sales`. Turn on **Always loaded** so the booking SOP is in every prompt without a `load_skill` round-trip.
  </Step>

  <Step>
    Scroll to **Data Components** and add `SalesScheduler`.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner.
  </Step>
</Steps>

**Prompt:**

```text
You help prospective customers understand [Your Product] and connect with the sales team.

# Sales intent
When the visitor shows sales intent, follow the SOP in the "book-meeting-with-sales" skill. On a sales-intent turn:
- Do NOT search the knowledge base, answer from docs, or save citations — even if the docs could answer. Surface the booking flow instead.

# Everything else
For genuine product or how-it-works questions:
1. Search the knowledge base before answering.
2. Save every fact you reference as a `citation` artifact before using it.
3. Cite via saved artifacts — never paste raw URLs.
4. If the docs don't cover it, say so plainly. Don't invent facts.

# Rules
- Render at most ONE data component per response.
- Put any intro text in plain text BEFORE the component, never wrapped inside it.

Tone: Direct, neutral, first person on behalf of the company.
```

<Tip>
  Replace `[Your Product]` with your product name. This prompt assumes you've already added the `citation` artifact from the [Citations](/guides/agents/customer-assistant/citations) guide.
</Tip>

## Render the scheduler and confirm the booking in your app

The agent emits a `SalesScheduler` component; your app renders it as a booking button that opens a Cal.com modal. After the visitor books, Cal.com fires a callback — you relay that back to the agent as a chat message so it can confirm the meeting. This is the [interactive components](/talk-to-your-agents/interactive-components) pattern: a `components` map plus `chatFunctionsRef` to submit on the user's behalf.

Because `SalesScheduler` is rendered by the Inkeep widget — not your own React tree — it can't call `chatFunctionsRef` directly. So it dispatches a window-level `inkeep-send-message` event; your host component listens for that event and relays it via `submitMessage`.

First, register the component and listen for the booking relay in your chat settings:

```tsx title="SalesIntentChat.tsx"
import { useEffect, useRef } from 'react';
import { InkeepEmbeddedChat } from '@inkeep/agents-ui-cloud';
import type {
  AIChatFunctions,
  InkeepAIChatSettings,
} from '@inkeep/agents-ui-cloud/types';
import SalesScheduler from './SalesScheduler';

export default function SalesIntentChat() {
  const chatRef = useRef<AIChatFunctions | null>(null);

  // The scheduler dispatches `inkeep-send-message` after a booking.
  // Relay it into the chat so the agent can confirm the meeting.
  useEffect(() => {
    const handler = (e: Event) => {
      const message = (e as CustomEvent).detail?.message;
      if (message) chatRef.current?.submitMessage(message);
    };
    window.addEventListener('inkeep-send-message', handler);
    return () => window.removeEventListener('inkeep-send-message', handler);
  }, []);

  const aiChatSettings: InkeepAIChatSettings = {
    appId: 'YOUR_APP_ID',
    chatFunctionsRef: chatRef,
    components: {
      SalesScheduler,
    },
  };

  return <InkeepEmbeddedChat aiChatSettings={aiChatSettings} />;
}
```

Then build the scheduler component. It opens the Cal.com calendar in a modal and, on a successful booking, dispatches `inkeep-send-message` with the meeting details:

```tsx title="SalesScheduler.tsx"
import { getCalApi } from '@calcom/embed-react';
import { useEffect } from 'react';

interface SalesSchedulerProps {
  calLink?: string;
  title?: string;
}

export default function SalesScheduler({ calLink, title }: SalesSchedulerProps) {
  const resolvedLink = (calLink || 'your-name/30min').replace(
    /^https?:\/\/cal\.com\//,
    ''
  );

  useEffect(() => {
    let cancelled = false;

    const handler = (e: { detail: { data: { title?: string; startTime?: string } } }) => {
      const { title: bookingTitle, startTime } = e.detail.data;
      let message = "I've just booked a meeting";
      if (startTime) {
        const formatted = new Date(startTime).toLocaleString('en-US', {
          weekday: 'long',
          month: 'long',
          day: 'numeric',
          hour: 'numeric',
          minute: '2-digit',
          timeZoneName: 'short',
        });
        message = `I've just booked a meeting for ${formatted}`;
      }
      if (bookingTitle) message += ` — "${bookingTitle}"`;
      message += '.';

      window.dispatchEvent(
        new CustomEvent('inkeep-send-message', { detail: { message } })
      );
    };

    (async () => {
      const cal = await getCalApi();
      if (cancelled) return;
      cal('preload', { calLink: resolvedLink });
      cal('on', { action: 'bookingSuccessfulV2', callback: handler });
    })();

    // The Cal.com embed API is a global singleton — remove the listener on
    // unmount so React Strict Mode's remount doesn't double-register it
    // (which would fire the booking message twice).
    return () => {
      cancelled = true;
      getCalApi().then((cal) =>
        cal('off', { action: 'bookingSuccessfulV2', callback: handler })
      );
    };
  }, [resolvedLink]);

  const openCalendar = async () => {
    const cal = await getCalApi();
    cal('modal', { calLink: resolvedLink, config: { layout: 'month_view' } });
  };

  return (
    <div className="rounded-xl border border-gray-200 bg-white p-5 my-2 w-full">
      <p className="text-sm font-semibold text-gray-900 mb-1">
        {title || 'Schedule a meeting'}
      </p>
      <p className="text-xs text-gray-600 mb-4">
        Pick a time that works — calendar invite sent automatically
      </p>
      <button
        type="button"
        onClick={openCalendar}
        className="px-5 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition"
      >
        Pick a time →
      </button>
    </div>
  );
}
```

<Note>
  Replace `YOUR_APP_ID` with the App ID from your project's **Apps** settings (see [App Credentials](/talk-to-your-agents/app-credentials)), and `your-name/30min` with your Cal.com event path.
</Note>

The round-trip: the visitor books → Cal.com fires `bookingSuccessfulV2` → your component dispatches `inkeep-send-message` → the listener calls `submitMessage` → the agent receives "I've just booked a meeting…" and confirms it, following step 3 of the skill.

## Test the full flow

<Steps>
  <Step>
    Click **Try it** in the top right corner of the Visual Builder. Ask a product question your docs cover — e.g. *"What does the product do?"*. The agent searches the knowledge base and answers with citations. No scheduler.
  </Step>

  <Step>
    Now show sales intent — *"How much does it cost?"* or *"Can I get a demo?"*. The agent responds with a short acknowledgement and emits the `SalesScheduler` component.
  </Step>

  <Step>
    In your embedded app, pick a time and book. Confirm the agent replies with a booking confirmation and offers to keep answering questions.
  </Step>
</Steps>

When working correctly, one agent carries the visitor from "what is this?" to a booked meeting and back to Q\&A — all in the same thread, no human handoff.

## Next steps

Add a second intent path — say, routing enterprise visitors to a different calendar or capturing a qualified lead — by dropping in another skill and a matching data component. The booking skill you built here is the template: trigger list, acknowledge-then-act SOP, one component to render. No prompt rewrite needed.


# Build a fact finder agent with code (/guides/agents/fact-finder)

Step-by-step tutorial to build a fact finder agent using the TypeScript SDK with web search and citations.



## Overview

In this tutorial, you'll build a fact finder agent using code and the Inkeep SDK. When you ask "What is Inkeep?", the agent will:

1. Search your knowledge base using the Inkeep RAG MCP tool
2. Present relevant facts about Inkeep

<Video src="youtube.com/watch?v=76HP-P269RI&feature=youtu.be" title="Fact finder agent tutorial" />

## Prerequisites

* An existing Inkeep project running (follow the [quick start guide](/get-started/quick-start) to get started)
* Or access to Inkeep Enterprise

## Setting up the project

### Step 1: Create the project directory

<Steps>
  <Step>
    Navigate to your projects directory:

    ```bash
    cd src/projects  
    ```
  </Step>

  <Step>
    Create a new directory for your fact finder project:

    ```bash
    mkdir fact-finder  
    ```
  </Step>

  <Step>
    Navigate into the new directory:

    ```bash
    cd fact-finder  
    ```
  </Step>
</Steps>

### Step 2: Create the project configuration

Create an `index.ts` file with the following content:

```typescript
import { project } from "@inkeep/agents-sdk";

export const myProject = project({
  id: "<project-id>",
  name: "<project-name>",
  description: "Fact finder project template",
  models: {
    base: {
      model: "<model-name>",
    },
    structuredOutput: {
      model: "<model-name>",
    },
    summarizer: {
      model: "<model-name>",
    },
  },
});
```

<Note>
  Replace the placeholder values (`<project-id>`, `<project-name>`, `<model-name>`) with your actual project details. If you need help recreating the boilderplate, you can reuse the index.ts file from the default activities planner project.
</Note>

### Step 3: Push the project to Visual Builder

<Steps>
  <Step>
    From the `fact-finder` directory, run:

    ```bash
    inkeep push  
    ```
  </Step>

  <Step>
    Verify in the Visual Builder that the project is created successfully.
  </Step>
</Steps>

## Creating the agent

### Step 1: Create the agent directory

```bash
mkdir agents  
```

### Step 2: Create the fact finder agent

Create `agents/fact-finder-agent.ts` with the following content:

```typescript
import { agent, subAgent } from "@inkeep/agents-sdk";
import { inkeepRagMcpTool } from "../tools/inkeep-rag-mcp";

const mySubAgent = subAgent({
  id: 'fact-finder-sub-agent',
  name: 'Fact Finder SubAgent',
  description: 'A sub agent that can help you find facts about Inkeep',
  prompt: 'You are a fact finder sub agent that can help you find facts about Inkeep',
  canUse: () => [inkeepRagMcpTool]
});

export const factFinderAgent = agent({
  id: "fact-finder-agent",
  name: "Fact Finder Agent",
  description: "A agent that can help you find facts about Inkeep",
  defaultSubAgent: mySubAgent,
  subAgents: () => [mySubAgent],
});
```

## Adding the MCP tool

### Step 1: Create the tools directory

```bash
mkdir tools  
```

### Step 2: Create the Inkeep RAG MCP tool

Create `tools/inkeep-rag-mcp.ts` with the following content:

```typescript
import { mcpTool } from "@inkeep/agents-sdk";

export const inkeepRagMcpTool = mcpTool({
  id: "inkeep-rag-mcp",
  name: "Inkeep RAG MCP",
  serverUrl: "https://mcp.inkeep.com/inkeep/mcp",
});
```

This tool enables your agent to search your Inkeep knowledge base for relevant information, leveraging Inkeep Unified Search's RAG capabilities to provide accurate and relevant results.

<Tip>
  Inkeep Unified Search is part of [Inkeep's Enterprise offering](https://inkeep.com/enterprise). Connect 25+ data sources to create a unified knowledge base that your agents can access.
</Tip>

### Step 3: Register the fact finder agent and rag mcp tool in the project

```typescript
import { project } from "@inkeep/agents-sdk";
import { factFinderAgent } from "./agents/fact-finder-agent";
import { inkeepRagMcpTool } from "./tools/inkeep-rag-mcp";

export const myProject = project({
  id: "<project-id>",
  name: "<project-name>",
  description: "Fact finder project template",
  agents: () => [factFinderAgent],
  tools: () => [inkeepRagMcpTool],
  models: {
    base: {
      model: "<model-name>",
    },
    structuredOutput: {
      model: "<model-name>",
    },
    summarizer: {
      model: "<model-name>",
    },
  },
});
```

## Deploying your agent

<Steps>
  <Step>
    From the `fact-finder` directory, run:

    ```bash
    inkeep push  
    ```
  </Step>

  <Step>
    Verify in the Visual Builder that the fact finder agent is created successfully.
  </Step>
</Steps>

## Testing your agent

<Steps>
  <Step>
    In the Visual Builder, click the **Try it** button to open the chat interface.
  </Step>

  <Step>
    Test your agent by asking questions like:

    * "What is Inkeep?"
    * "Tell me about Inkeep's features"
    * "How does Inkeep work?"
  </Step>
</Steps>

When working correctly, the agent will search your knowledge base and present relevant facts about Inkeep.


# Build an in-product copilot without code (/guides/agents/in-product-copilot)

Build a logged-in, account-aware copilot that diagnoses live account, integration, and usage state and resolves issues in-product — without escalating to a human.



## Overview

> **Who this is for:** product teams adding a copilot for **logged-in end users**, embedded **inside your application**. (For a copilot your **support agents** run inside the **Zendesk ticket sidebar**, see [Support Copilot](/guides/agents/support-copilot/setup).)

In this tutorial, you'll build an in-product copilot using the Inkeep Visual Builder. It's embedded in your application, acts on behalf of the signed-in user, and — instead of handing problems to a human — **diagnoses and fixes them in product**.

When a user says *"My Slack integration stopped working and we can't add new users"*, the copilot will:

1. Pull live account state in one turn — the account, the integration's health, current usage and seats
2. Render what it found as cards, then **act**: rotate the broken credential, recommend the right plan
3. Answer "how do I…" questions from your docs when the user needs guidance rather than a fix

It knows who the user is because your application forwards the signed-in user's credentials to your MCP server on every tool call.

<Image src="/images/in-product-copilot-flow.png" alt="Header forwarding flow: Your App sends custom headers to the Inkeep Agent, which resolves templates and forwards credentials to Your MCP Server" />

## Prerequisites

* An existing Inkeep Visual Builder instance running (follow the [quick start guide](/get-started/quick-start) to get started)
* Or access to [Inkeep Enterprise](https://inkeep.com/demo)
* A deployed MCP server that exposes your product's account operations and accepts per-user auth tokens (see [Build Custom MCP Servers](/guides/mcp-servers/custom-mcp-servers) if you need to create one). For this copilot it should expose:
  * **Reads** — look up the account, its usage metrics, and an integration's status
  * **Writes** — rotate an integration credential, change the plan
  * **Recommendation** — suggest the right plan from current usage
* *(Optional)* Inkeep Unified Search configured, so the copilot can also answer conceptual "how do I…" questions from your docs

## Set up the agent

<NumberedStepsTOC stepIds={["create-the-copilot-agent", "define-the-headers-schema", "configure-the-sub-agent"]} />

<Steps>
  <Step>
    ### Create the copilot agent

    Go to the **Agents** tab in the left sidebar, then select **Create Agent**. Provide the following details:

    * **Name**: `Product copilot`
    * **Description**: `An in-product copilot that diagnoses and resolves account, integration, and usage issues`
  </Step>

  <Step>
    ### Define the headers schema

    The headers schema tells Inkeep which headers to expect and validate on every incoming request. These values come from your host application at runtime and identify the signed-in user.

    On the agent settings pane, scroll down to **Headers schema** and enter the following:

    ```json
    {
      "$schema": "http://json-schema.org/draft-07/schema#",
      "type": "object",
      "properties": {
        "auth-token": {
          "type": "string",
          "description": "The signed-in user's API token, forwarded to MCP servers."
        },
        "user-id": {
          "type": "string",
          "description": "The signed-in user's ID."
        }
      },
      "required": ["auth-token", "user-id"],
      "additionalProperties": {}
    }
    ```

    Click **Save** to apply the schema.

    <Warning>
      Header keys are normalized to **lowercase** before validation. Always define schema properties in lowercase (e.g., `auth-token`, not `authToken`).
    </Warning>
  </Step>

  <Step>
    ### Configure the sub agent

    Click on the **Default Sub Agent** to configure it. Provide the following details:

    * **Name**: `Product copilot`
    * **Description**: `Diagnose account, integration, and usage state and resolve issues in product`
    * **Prompt**: Copy the prompt below

    **Prompt:**

    ```text
    You are an in-product copilot for [Your Product], assisting the signed-in user (id {{headers.user-id}}). You can see their live account state through your tools. Your job is to diagnose issues and RESOLVE them in product — you do not escalate to a human, you act.

    How to handle a turn:
    1. Chain reads aggressively. When the question implies several lookups ("Slack is broken AND we can't add users"), call all the relevant read tools BEFORE rendering anything.
    2. Render the diagnostic cards (Account Overview and/or Integration Health Check) with the real values the tools returned.
    3. State in one short sentence what you found and what you're about to do.
    4. Call the write tool to fix it, then render an Action Result confirming the change.
    5. If usage or seats suggest an upgrade, recommend the right plan and render a Plan Recommendation. If the user accepts ("yes", "upgrade"), change the plan and confirm.

    If a knowledge-base search tool is connected, use it for "how do I…" or conceptual questions and answer from the docs — and if the answer reveals something fixable on this account, resolve it rather than just linking a doc. If no knowledge-base tool is connected, say you can only help with account operations rather than guessing.

    Rules:
    - Never render a card without first calling the tool behind it. Use real returned values — never placeholders.
    - Execute routine writes (e.g. rotating a credential) without asking — just do it and confirm with an Action Result.
    - For hard-to-reverse writes (plan changes, seat removals, deletions), render a recommendation card first and wait for the user to accept ("yes", "upgrade") before executing.
    - Never invent account details, IDs, or statuses.
    ```

    <Tip>
      The `{{headers.user-id}}` template is resolved at runtime from the validated request headers. You can reference any key defined in the headers schema. Replace `[Your Product]` with your product name.
    </Tip>
  </Step>
</Steps>

## Register and connect your MCP servers

<NumberedStepsTOC stepIds={["register-the-product-mcp-server", "connect-it-with-header-forwarding", "optional-add-knowledge-base-search"]} />

<Steps>
  <Step>
    ### Register the Product MCP server

    Because each user has their own token, you don't store a single static credential. Instead, credentials are forwarded dynamically from the incoming request headers.

    Go to the **MCP Servers** tab in the left sidebar, then select **+ New MCP Server**. Fill in the server details:

    * **Name**: `Product API`
    * **URL**: The URL of your deployed MCP server (e.g., `https://api.yourproduct.com/mcp`)
    * **Credential**: Select **No Authentication**

    Click **Create** to register the server.

    <Note>
      Leaving the credential blank is intentional. Auth will be injected per-request via header forwarding in the next step.
    </Note>
  </Step>

  <Step>
    ### Connect it with header forwarding

    Navigate to the agent canvas: **Agents** → **Product copilot**. Drag and drop an **MCP** block from the top left toolbar onto the canvas, then select the **Product API** MCP server and connect it to the **Product copilot** sub agent.

    Click on the connection line between the MCP server and the sub agent. In the **Headers** field, enter:

    ```json
    {
      "Authorization": "Bearer {{headers.auth-token}}",
      "X-User-Id": "{{headers.user-id}}"
    }
    ```

    Click **Save Changes** in the top right corner.

    The `{{headers.*}}` templates are resolved at runtime from the validated incoming request headers. On every tool call to the MCP server, Inkeep replaces the template with the actual header value from the current request — so the copilot always acts as the signed-in user.
  </Step>

  <Step>
    ### (Optional) Add knowledge base search

    To let the copilot answer "how do I…" and conceptual questions from your docs, also connect [Inkeep Unified Search](/connect-your-data/inkeep) as an MCP server and attach it to the same sub agent. The copilot will reach for it on knowledge questions and stay on the account tools for diagnosis and fixes.
  </Step>
</Steps>

## Add structured cards

The copilot's diagnosis and fixes land as rich cards, not walls of text. You define these as **data components**: name each one and give it a JSON schema, and the agent fills it in with real tool results. Create them in the **Data Components** tab, then attach them to the sub agent — see [Data Components](/visual-builder/structured-outputs/data-components) for the full walkthrough.

This copilot uses four:

* **Account Overview** — plan, seats, usage, and renewal date, rendered after looking up the account.
* **Integration Health Check** — the status of one integration (healthy, degraded, down) and the recommended action.
* **Action Result** — a confirmation card after a write, e.g. "Slack credential rotated" — the "we fixed it" moment.
* **Plan Recommendation** — the right plan with reasons and new limits, which the user can accept to trigger an upgrade.

## Embed the copilot in your application

Pass the signed-in user's credentials as custom headers from your chat component. The chat component sends these headers with every request to the agent API.

<Tabs>
  <Tab title="React">
    ```tsx
    import { InkeepEmbeddedChat } from '@inkeep/agents-ui-cloud';
    import type { InkeepAIChatSettings } from '@inkeep/agents-ui-cloud/types';

    function Copilot({ authToken, userId }: { authToken: string; userId: string }) {
      const aiChatSettings: InkeepAIChatSettings = {
        appId: 'YOUR_APP_ID',
        headers: {
          'auth-token': authToken,
          'user-id': userId,
        },
      };

      return <InkeepEmbeddedChat aiChatSettings={aiChatSettings} />;
    }
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript
    const chat = Inkeep.EmbeddedChat('#ikp-embedded-chat-target', {
      aiChatSettings: {
        appId: 'YOUR_APP_ID',
        headers: {
          'auth-token': currentUser.authToken,
          'user-id': currentUser.id,
        },
      },
    });
    ```
  </Tab>
</Tabs>

<Note>
  Replace `YOUR_APP_ID` with the App ID from your project's **Apps** settings. See [App Credentials](/talk-to-your-agents/app-credentials) for setup details.
</Note>

## Test the copilot

<NumberedStepsTOC stepIds={["test-in-the-visual-builder-playground", "test-in-your-application"]} />

<Steps>
  <Step>
    ### Test in the Visual Builder playground

    Click **Try it** in the top right corner to open the chat interface. Click the **Custom headers** button and enter test values:

    ```json
    {
      "auth-token": "test-token-abc123",
      "user-id": "user-42"
    }
    ```

    Click **Apply**, then send a message like *"My Slack integration stopped working and we can't add new users — can you fix it?"*

    The copilot should chain its read tools (account, integration status, usage), render the Account Overview and Integration Health Check cards, rotate the Slack credential, confirm with an Action Result, and — if seats are maxed — render a Plan Recommendation. Verify in your MCP server logs that the `Authorization` and `X-User-Id` headers arrive populated from the values you entered.
  </Step>

  <Step>
    ### Test in your application

    After embedding the chat component, sign in as a test user and interact with the copilot. Confirm that:

    1. The agent responds with data scoped to the signed-in user
    2. Your MCP server logs show the correct auth token and user ID on each tool call
    3. Switching users changes the data the copilot returns and acts on
  </Step>
</Steps>


# Build a meeting prep agent without code (/guides/agents/meeting-prep-assistant)

Step-by-step tutorial to build a meeting prep agent using the Visual Builder with Google Calendar and web search tools.



## Overview

In this tutorial, you'll build a meeting prep assistant agent using the Inkeep Visual Builder. When you ask "Can you help me prep for my upcoming Acme meeting?", the agent will:

1. Look up the meeting in your Google Calendar
2. Search the web to research the company
3. Present a comprehensive meeting prep summary

<Video src="https://www.youtube.com/watch?v=8DxeNhfKJco" title="Meeting prep agent tutorial" />

## Prerequisites

* An existing Inkeep Visual Builder instance running (follow the [quick start guide](/get-started/quick-start) to get started)
* Or access to Inkeep Enterprise

## Creating the Sub Agents

### Step 1: Create the meeting prep agent

<Steps>
  <Step>
    Go to the **Agents** tab in the left sidebar, then select **Create Agent**.
  </Step>

  <Step>
    Provide the following details:

    * **Name**: `Meeting prep agent`
    * **Description**: `An agent that can help you prepare for your upcoming meeting`
  </Step>
</Steps>

### Step 2: Configure the meeting prep coordinator sub agent

The default sub agent will serve as the coordinator that orchestrates the workflow.

<Steps>
  <Step>
    Click on the **Default Sub Agent** to configure it.
  </Step>

  <Step>
    Provide the following details:

    * **Name**: `Meeting prep coordinator`
    * **Description**: `Orchestrate specialized agents to prepare for a meeting`
    * **Prompt**: Copy the prompt below
  </Step>
</Steps>

**Prompt:**

```
Orchestrate specialized agents to prepare for a meeting.

<workflow>
1. Greet & Find Meeting:
   - Greet user, understand which company
   - Announce: "Finding meeting with [Company]..."
   - Delegate to Meeting Finder
   - VERBOSE: Summarize meeting found (date, time, participants)

2. Company Research:
   - Announce: "Researching [Company]..."
   - Delegate to Company research
   - VERBOSE: Summarize company insights (what they do, products)

3. Create Prep:
   - Announce: "Creating prep summary..."
   - Encouraging closing message
</workflow>

<rules>
- Always delegate in order: Meeting Finder → Company Research
- BE VERBOSE after each delegation returns
- Show progress and insights clearly
- Proceed automatically
</rules>
```

### Step 3: Create the meeting finder sub agent

<Steps>
  <Step>
    Drag and drop a **Sub Agent** block from the top left toolbar onto the canvas.
  </Step>

  <Step>
    Configure the sub agent with:

    * **Name**: `Meeting finder`
    * **Description**: `Find the external meeting to prep for using Google Calendar`
    * **Prompt**: Copy the prompt below
  </Step>

  <Step>
    Connect the Meeting Finder sub agent to the Meeting Prep Coordinator.
  </Step>

  <Step>
    Click on the connector and change the relationship type from **Transfer** to **Delegate**.
  </Step>
</Steps>

**Prompt:**

```
Find the external meeting to prep for using Google Calendar.

<workflow>
1. Search upcoming meetings for target company
2. Filter: ONLY meetings with external email domain (e.g., @nvidia.com)
3. Skip internal-only meetings entirely
4. Present first external meeting:
   - Date/time, duration
   - Title and link
   - External attendees (name + email)
   - Internal team (list all @inkeep.com emails)
5. Return to coordinator
</workflow>

<rules>
- Never mention internal meetings
- Automatically use first external meeting found
- List all internal participant emails explicitly
- Return to coordinator after finding meeting
- Do not pass in a start_time parameter
</rules>
```

### Step 4: Create the company research sub agent

<Steps>
  <Step>
    Drag and drop another **Sub Agent** block onto the canvas.
  </Step>

  <Step>
    Configure the sub agent with:

    * **Name**: `Company research`
    * **Description**: `Research the company to understand what they do`
    * **Prompt**: Copy the prompt below
  </Step>

  <Step>
    Connect the Company Research sub agent to the Meeting Prep Coordinator using the connector.

    <Step>
      Click on the connector and change the relationship type from **Transfer** to **Delegate**.
    </Step>
  </Step>

  <Step>
    Click **Save Changes** in the top right corner to save your agent configuration.
  </Step>
</Steps>

**Prompt:**

```
Research the company to understand what they do.

<workflow>
1. Exa: Scrape company website
   - Show key info found
2. Analyze:
   - What does company do?
   - Key products/services
   - Market position
3. Present summary with talking points
4. Return to coordinator
</workflow>

<rules>
- Brief explanations under 200 chars
- Show findings immediately
- Proceed automatically
</rules>
```

## Adding MCP tools

To enable your agent to access Google Calendar and perform web research, you'll need to add MCP tools. We'll use Zapier MCP, which provides pre-built MCP servers for many popular services.

<Tip>
  Learn more on how to create MCP servers in our [MCP Servers guide](/guides/mcp-servers/overview).
</Tip>

### Step 1: Create a Google Calendar MCP server

<Steps>
  <Step>
    Create a Zapier MCP account [here](https://zapier.com/mcp).
  </Step>

  <Step>
    Click **+ New MCP Server** in the top right corner. When prompted to select an MCP Client, choose **Other**.
  </Step>

  <Step>
    Click **+ Add Tool** and select **Google Calendar**. Then select the **Find Events** tool, click **Connect**, and complete the OAuth flow to authorize access.
  </Step>

  <Step>
    Navigate to the **Connect** tab and copy the **Server URL**. You'll need this to register the tool in the Visual Builder.
  </Step>
</Steps>

### Step 2: Register MCP servers in Visual Builder

<Steps>
  <Step>
    In the Visual Builder, go to the **MCP Servers** tab in the left sidebar, then select **+ New MCP Server**.
  </Step>

  <Step>
    Select **Custom Server** and enter the Server URL you copied from Zapier. Give it a descriptive name (e.g., "Google Calendar"), and for **Credential**, select **No Authentication**.
  </Step>

  <Step>
    Click **Save** to register the Google Calendar server.
  </Step>

  <Step>
    Repeat the same process for the Exa MCP Server. The MCP Server URL is [https://mcp.exa.ai/mcp](https://mcp.exa.ai/mcp).
  </Step>
</Steps>

### Step 3: Connect tools to sub agents

<Steps>
  <Step>
    Navigate to the agent canvas: **Agents** > **Meeting prep agent**.
  </Step>

  <Step>
    Drag and drop an **MCP** block from the top left toolbar onto the canvas.
  </Step>

  <Step>
    Select the **Google Calendar MCP** tool and connect it to the **Meeting Finder** sub agent.
  </Step>

  <Step>
    Add another MCP block, select the **Exa MCP** tool, and connect it to the **Company Research** sub agent.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner to save your configuration.
  </Step>
</Steps>

## Testing your agent

Ensure you have at least one upcoming meeting with an external company on your Google Calendar. If you don't have one, create a test meeting.

<Steps>
  <Step>
    Click **Try it** in the top right corner to open the chat interface.
  </Step>

  <Step>
    Test your agent by asking: "Can you help me prep for my upcoming \[company name] meeting?"
  </Step>
</Steps>

When working correctly, the agent will:

1. Find the meeting in your Google Calendar
2. Research the company using Exa
3. Present a comprehensive meeting prep summary

<Image src="/gifs/agent-in-action.gif" alt="Meeting prep agent in action" />


# Leverage Composio MCP Server Library (/guides/mcp-servers/composio-mcp-servers)

Connect to Composio MCP servers for access to 10,000+ out-of-box integrations



## Overview

[Composio](https://composio.dev/) provides access to over 10,000 pre-built MCP servers for popular services and integrations.

<BigVideo src="/videos/composio-inkeep.mp4" />

### Step 1: Create a Composio Account

Sign up for a Composio account at [composio.dev](https://composio.dev/)

### Step 2: Configure Your MCP Servers

After creating a new organization and then a new project, navigate to the MCP configurations section and create configs for the MCP servers you want to use.

<img src="/images/composio-create-config.png" alt="Composio MCP Configs" style={{ borderRadius: '8px' }} />

### Step 3: Retrieve Your API Key

Copy your Composio API key from the Settings tab.

<img src="/images/composio-api-key.png" alt="Composio API key" style={{ borderRadius: '8px' }} />

### Step 4: Add API Key to Environment

Add your Composio API key to your root `.env` file:

```
COMPOSIO_API_KEY=your-composio-api-key
```

### Step 5: Restart and Verify

Restart your development servers by pressing Control + C and running `pnpm dev` again (pressing `r` will not work).

Open the Visual Builder and navigate to the MCP Servers tab. Click **+New MCP Server**, then select the **Popular MCP Servers** tab and search for "composio" to see your configured Composio MCP servers.


# Build Custom MCP Servers (/guides/mcp-servers/custom-mcp-servers)

Build your own custom MCP servers



## When to Build Custom MCP Servers

Build custom MCP servers when:

* Native servers don't exist for your specific APIs
* You need custom business logic beyond what existing servers provide
* You have proprietary systems that require custom integration

If pre-built servers meet your needs, consider [Native MCP servers](/guides/mcp-servers/native-mcp-servers), [Composio's platform](/guides/mcp-servers/composio-mcp-servers), or [Gram](/guides/mcp-servers/gram) instead.

## Getting Started

The Quick Start workspace includes a Next.js app in the `apps/mcp/app/` directory that you can use to expose your MCP servers.
Each MCP server you create will be exposed on a separate route on this app.

#### Creating from a template

You can add a custom MCP server template from our [template library](https://github.com/inkeep/agents/tree/main/agents-cookbook/template-mcps) for common use cases using the [CLI](/typescript-sdk/cli-reference#inkeep-add).

```bash
inkeep add --mcp [server-name]
```

This will automatically add a custom MCP server template to your Quick Start workspace. After customizing and deploying it, you can register it.

#### Using Vercel's Template

If you want to create a custom MCP server, you can use Vercel's [Next.js MCP template](https://vercel.com/templates/next.js/model-context-protocol-mcp-with-next-js) as a starting point:

<Steps>
  <Step>
    From the workspace root, add the Vercel template:

    ```bash
    inkeep add --mcp vercel-template
    ```
  </Step>

  <Step>
    Rename the template to your desired server name:

    ```bash
    mv apps/mcp/app/vercel-template apps/mcp/app/[server-name]
    ```
  </Step>

  <Step>
    Update the `basePath` inside `apps/mcp/app/[server-name]/mcp/route.ts` to `/[server-name]`:

    ```typescript
    {
      basePath: "/[server-name]",
      verboseLogs: true,
      maxDuration: 60,
      disableSse: true,
    }
    ```
  </Step>

  <Step>
    Customize the template with your desired tools.
  </Step>

  <Step>
    You can access the server at `http://localhost:3006/[server-name]/mcp`. Register it with your agents using the `mcpTool` function.

    ```typescript
    import { mcpTool } from '@inkeep/agents-sdk';

    export const mcpTool = mcpTool({
      id: '[server-name]',
      name: '[server-name]',
      description: '[server-name] API',
      serverUrl: 'http://localhost:3006/[server-name]/mcp',
    });
    ```

    Or you register the MCP server as a tool in the Visual Builder (see [here](/visual-builder/tools/mcp-servers#register-an-mcp-server)).
  </Step>
</Steps>

#### Deploying to Vercel

Follow the instructions in [Deploy to Vercel](/deployment/vercel#step-10-create-a-vercel-project-for-your-mcp-server-optional).
If you have already deployed to Vercel, you can simply update the deployment by pushing to the same repository.

<Note>
  Enable Fluid compute and set `maxDuration` to 800 in `apps/mcp/app/[server-name]/[transport]/route.ts` if you're on a Pro or Enterprise plan and have long-running operations.
</Note>

<>
  ### Next steps

  Once you have your server, you can register it in one of:

  <Cards>
    <Card title="The TypeScript SDK" icon="LuCode" href="/typescript-sdk/tools/mcp-tools#step-2-register-the-mcp-server-as-a-tool-in-your-agent">
      Add your MCP server as a tool for your agents in the TypeScript SDK
    </Card>

    <Card title="The Visual Builder" icon="LuPalette" href="/visual-builder/tools/mcp-servers#register-an-mcp-server">
      Add your MCP server as a tool for your agents in the Visual Builder
    </Card>
  </Cards>
</>


# Create an OpenAPI-based MCP server (/guides/mcp-servers/gram)

Generate an MCP server from an OpenAPI spec using Gram and connect it to your Inkeep agents.



## Overview

Gram by Speakeasy turns any OpenAPI specification into a production-ready MCP server. Upload your API spec, and Gram automatically generates tool definitions, handles hosting and authentication, and deploys your server- so you can focus on building powerful agents.

Visit [Gram's documentation](https://www.speakeasy.com/docs/gram/getting-started/openapi) for a complete guide.

<Tip>
  For custom MCP server logic, use [Gram Functions](https://www.speakeasy.com/docs/gram/getting-started/typescript), Gram's TypeScript framework with managed deployment and hosting.
</Tip>

### Step 1: Sign up for a Gram account

[Sign up](https://app.getgram.ai/login) for a free Gram account.

### Step 2: Upload your OpenAPI document

<Steps>
  <Step>
    Navigate to **Toolsets** in the Gram dashboard sidebar (under **Create**) and click **Get Started**
  </Step>

  <Step>
    Upload your OpenAPI document, provide a descriptive name, and click **Upload**
  </Step>
</Steps>

Gram extracts URL paths, HTTP methods, request schemas, and operation descriptions from your OpenAPI document to generate tool definitions.

<Note>
  Gram works with partial OpenAPI documents as long as key elements (paths, methods, schemas, descriptions) are present.
</Note>

### Step 3: Ship 🚢

Your MCP server is ready to ship! Open the MCP page, and open your MCP server by clicking it. Click Enable to allow the MCP server to handle requests.

### Step 4: Add authentication (optional)

To add authentication, set the appropriate environment variables. See [Step 3 in the Gram documentation](https://www.speakeasy.com/docs/gram/getting-started/openapi#step-3-set-environment-variables) for details.

<>
  ### Next steps

  Once you have your server, you can register it in one of:

  <Cards>
    <Card title="The TypeScript SDK" icon="LuCode" href="/typescript-sdk/tools/mcp-tools#step-2-register-the-mcp-server-as-a-tool-in-your-agent">
      Add your MCP server as a tool for your agents in the TypeScript SDK
    </Card>

    <Card title="The Visual Builder" icon="LuPalette" href="/visual-builder/tools/mcp-servers#register-an-mcp-server">
      Add your MCP server as a tool for your agents in the Visual Builder
    </Card>
  </Cards>
</>


# Connect to Native MCP Servers (/guides/mcp-servers/native-mcp-servers)

Connect to Native MCP servers provided by SaaS vendors



The fastest way to get started is by connecting to existing MCP servers provided directly by SaaS providers that offer them.

### MCPs with OAuth 2.1/PKCE support

These servers often implement OAuth 2.1 with PKCE for secure authentication that allow for `1-click` authentication without the need for API keys.

<>
  Here's an example list of popular service-maintained MCP Servers that support **1-click** install via the OAuth 2.1/PKCE authentication flow.

  | Name                                                                                                             | URL                                                                  |
  | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
  | [Atlassian (SSE)](https://support.atlassian.com/rovo/docs/getting-started-with-the-atlassian-remote-mcp-server/) | [https://mcp.atlassian.com/v1/sse](https://mcp.atlassian.com/v1/sse) |
  | [Canva](https://www.canva.dev/docs/connect/canva-mcp-server-setup/)                                              | [https://mcp.canva.com/mcp](https://mcp.canva.com/mcp)               |
  | [Hugging Face](https://huggingface.co/mcp)                                                                       | [https://huggingface.co/mcp?login](https://huggingface.co/mcp?login) |
  | [Intercom](https://developers.intercom.com/docs/guides/mcp)                                                      | [https://mcp.intercom.com/mcp](https://mcp.intercom.com/mcp)         |
  | [Linear](https://linear.app/docs/mcp)                                                                            | [https://mcp.linear.app/mcp](https://mcp.linear.app/mcp)             |
  | [Neon](https://neon.com/docs/ai/neon-mcp-server)                                                                 | [https://mcp.neon.tech/mcp](https://mcp.neon.tech/mcp)               |
  | [Notion](https://developers.notion.com/docs/get-started-with-mcp)                                                | [https://mcp.notion.com/mcp](https://mcp.notion.com/mcp)             |
  | [Prisma](https://www.prisma.io/docs/postgres/integrations/mcp-server#remote-mcp-server)                          | [https://mcp.prisma.io/mcp](https://mcp.prisma.io/mcp)               |
  | [Sentry](https://docs.sentry.io/product/sentry-mcp/)                                                             | [https://mcp.sentry.dev/mcp](https://mcp.sentry.dev/mcp)             |
  | [Vercel](https://vercel.com/docs/mcp/vercel-mcp)                                                                 | [https://mcp.vercel.com](https://mcp.vercel.com)                     |

  To add them to your project:

  1. navigate to **MCP Servers** in the Inkeep Visual Builder.
  2. Click on **New MCP Server**
  3. Choose from the preset list under **Popular Services** or register any under **Custom Server**

  When you register the server, an authentication pop-up will appear and allow you to sign in. Your credentials will automatically be saved to your [Nango Store](/typescript-sdk/credentials/nango) or [Keychain](/typescript-sdk/credentials/keychain) credential store.
</>

<>
  ### Next steps

  Once you have your server, you can register it in one of:

  <Cards>
    <Card title="The TypeScript SDK" icon="LuCode" href="/typescript-sdk/tools/mcp-tools#step-2-register-the-mcp-server-as-a-tool-in-your-agent">
      Add your MCP server as a tool for your agents in the TypeScript SDK
    </Card>

    <Card title="The Visual Builder" icon="LuPalette" href="/visual-builder/tools/mcp-servers#register-an-mcp-server">
      Add your MCP server as a tool for your agents in the Visual Builder
    </Card>
  </Cards>
</>


# MCP Server Options (/guides/mcp-servers/overview)

Learn about the recommended ways to use MCP servers with your agents



MCP servers enable your agents to fetch real-time information and take action on your systems, services, and applications.

## Choose your approach

Select the option that best fits your needs:

* **Native MCP Servers** - Pre-built servers from SaaS vendors (Atlassian, Linear, Notion, etc.). Fastest setup, just register and go.
* **Composio MCP Servers** - Access 10,000+ pre-built servers through Composio's platform. Similar to Native, but with a wider selection.
* **Custom MCP Servers** - Build your own servers for custom APIs and business logic. Requires development and deployment.
* **Gram by Speakeasy** - Convert OpenAPI specs into MCP servers using Gram by Speakeasy.

## Get started

<Cards>
  <Card title="Native MCP Servers" icon="LuPackageCheck" href="/guides/mcp-servers/native-mcp-servers">
    Connect to pre-built servers from providers like Atlassian, Linear, Notion, and more. No building required.
  </Card>

  <Card title="Composio MCP Servers" icon="brand/Composio" href="/guides/mcp-servers/composio-mcp-servers">
    Access thousands of out-of-box MCP servers through Composio's platform.
  </Card>

  <Card title="Custom MCP Servers" icon="brand/Inkeep" href="/guides/mcp-servers/custom-mcp-servers">
    Build your own custom MCP servers using Inkeep templates or Vercel's Next.js template.
  </Card>

  <Card title="Gram" icon="brand/Gram" href="/guides/mcp-servers/gram">
    Convert OpenAPI specs into MCP servers using Gram by Speakeasy.
  </Card>
</Cards>


# CLI Tutorials (/guides/cli/overview)

Learn how to use the Inkeep CLI for common workflows



The Inkeep CLI is your primary tool for managing your Inkeep agents and MCP servers. These guides walk you through common CLI workflows.

## What you'll learn

<Cards>
  <Card title="Set Up a CLI Profile" icon="LuUserCog" href="/guides/cli/setup-profile">
    Create a profile and authenticate with a remote deployment (only needed for remote push/pull)
  </Card>

  <Card title="Push to Remote" icon="LuCloudUpload" href="/guides/cli/push-to-remote">
    Push your local agent configurations to remote Inkeep instance
  </Card>

  <Card title="Pull from Remote" icon="LuCloudDownload" href="/guides/cli/pull-from-remote">
    Pull agent configurations from remote Inkeep instance to your local project
  </Card>
</Cards>

## Prerequisites

Before starting these tutorials, ensure you have:

* The Inkeep CLI installed globally:

```bash
npm install -g @inkeep/agents-cli
```

* Access to a remote Inkeep instance (e.g. [Inkeep Enterprise](https://inkeep.com/enterprise?cta_id=docs_nav) or a [self-hosted deployment](/deployment/vercel))

## Quick reference

For a complete list of CLI commands and options, see the [CLI Reference](/typescript-sdk/cli-reference).


# Pull from remote Inkeep instance (/guides/cli/pull-from-remote)

Pull agent configurations from a remote Inkeep instance, resolve merge conflicts interactively, and sync your local TypeScript project



This tutorial walks you through pulling agent configurations from a remote Inkeep instance to your local project. Use pull to bootstrap a local project from the [Visual Builder](/visual-builder/sub-agents), sync remote changes made by teammates, or set up a new development environment.

## Prerequisites

* Access to a remote Inkeep instance (e.g. [Inkeep Enterprise](https://inkeep.com/enterprise?cta_id=docs_nav) or a [self-hosted deployment](/deployment/vercel))
* The Inkeep CLI installed globally:

```bash
npm install -g @inkeep/agents-cli
```

* A CLI profile configured and authenticated (only needed for remote deployments). If you haven't done this yet, follow the [Set up a CLI profile](/guides/cli/setup-profile) tutorial.
* An LLM API key set as an environment variable. The pull command uses LLM generation to produce TypeScript files. Set one of the following:

```bash
export ANTHROPIC_API_KEY=your-key    # Recommended
export OPENAI_API_KEY=your-key       # Alternative
export GOOGLE_GENERATIVE_AI_API_KEY=your-key  # Alternative
```

## Step 1: Choose your pull target

You have two options depending on whether the project already exists locally.

### Option A: Pull into an existing project

If you already have a local project directory with an `index.ts` file, navigate into it:

```bash
cd my-project
```

### Option B: Pull a new project from the remote instance

If you don't have the project locally yet, you'll specify the project ID in the next step. The CLI creates a new directory for it automatically.

## Step 2: Run the pull command

<Note>
  Run `inkeep pull` from the directory that contains your `inkeep.config.ts`, or from any subdirectory below it.
</Note>

### For an existing project

From inside your project directory, run:

```bash
inkeep pull
```

The CLI will:

1. Detect the project from your `index.ts` file
2. Resolve configuration — your active [CLI profile](/guides/cli/setup-profile) overrides `inkeep.config.ts` for API URLs, API key, and tenant ID (see [Configuration Priority](/typescript-sdk/cli-reference#configuration-priority))
3. Fetch the latest configuration from the remote instance
4. Compare remote changes against your local files
5. Generate updated TypeScript files, preserving your local code structure

<Tip>
  The pull command uses smart comparison to detect what changed. If nothing has changed remotely, it tells you the project is already up to date.
</Tip>

### For a new project

Specify the project ID:

```bash
inkeep pull --project my-project-id
```

This creates a `./my-project-id/` directory with the full TypeScript project structure:

```
my-project-id/
├── index.ts                 # Project definition and wiring
├── agents/                  # Agent definitions
│   ├── my-agent.ts
│   └── sub-agents/
│       └── my-sub-agent.ts
├── tools/                   # Tool definitions
│   ├── my-tool.ts
│   └── functions/
│       └── my-function.ts
├── credentials/             # Credential references
│   └── my-credential.ts
├── environments/            # Environment-specific settings
│   ├── development.env.ts
│   └── index.ts
└── data-components/         # Data component definitions
    └── my-data-component.ts
```

## Step 3: Resolve merge conflicts

When your local state has diverged from the remote instance, `inkeep pull` detects conflicts and launches an interactive terminal UI so that you can resolve them.

### Interactive conflict resolution

The resolver presents conflicts one at a time. Each conflict shows the **entity type**, **entity ID**, and what each side did (added, modified, or deleted).

#### Row-level default

You can set a row level default to either **ours** or **theirs**. **Ours** refers to your local state, and **theirs** refers to the remote state. This default will be applied to all columns on the entity.

#### Column-level overrides

When both sides modified the same entity, you can resolve at a **per-column** granularity. Set a row default, then optionally override individual columns using the arrow keys.

<Image src="/images/conflict-resolution-example.png" alt="Interactive conflict resolution UI showing per-column picks for a sub_agents entity with name, description, and prompt fields" />

#### Keyboard controls

| Key           | Action                                        |
| ------------- | --------------------------------------------- |
| `↑` / `k`     | Move focus up towards the row default         |
| `↓` / `j`     | Move focus down through columns               |
| `←` / `1`     | Pick **ours** for the focused row or column   |
| `→` / `2`     | Pick **theirs** for the focused row or column |
| `Enter` / `n` | Next conflict                                 |
| `p`           | Previous conflict                             |
| `Esc` / `q`   | Cancel the pull                               |

#### Review and confirm

After resolving all conflicts, a summary screen shows your choices:

<Image src="/images/conflict-resolution-summary-example.png" alt="Interactive conflict resolution summary UI showing the resolutions for a set of conflicts" />

Press **Enter** to apply the resolutions and continue the pull, **p** to go back and adjust, or **Esc**/**q** to cancel.

### Auto-resolve conflicts

To skip the interactive UI and resolve all conflicts automatically, use the `--conflict-strategy` flag:

```bash
# Keep all local changes
inkeep pull --conflict-strategy ours

# Accept all remote changes
inkeep pull --conflict-strategy theirs
```

## Step 4: Review the generated files

After pulling (and resolving any conflicts), the CLI runs a two-stage validation:

1. **File verification** -- checks that all expected files exist with correct naming
2. **Round-trip validation** -- loads the generated TypeScript, serializes it back to JSON, and compares with the original remote data to ensure no data loss

If validation passes, you'll see:

```
✓ Basic file verification passed
✓ Round-trip validation passed - generated TS matches backend data
```

Review the generated files to confirm they look correct. If validation fails, the CLI reports specific differences so you can investigate.

## Step 5: Use the pulled project

Once pulled, your project is a standard TypeScript project using `@inkeep/agents-sdk` builder APIs. You can:

* Edit agent configurations in code
* Push changes back with `inkeep push`
* Run the Visual Builder locally with `inkeep dev`

## Additional options

### Pull all projects

To pull every project from your tenant at once:

```bash
inkeep pull --all
```

Each project is created as a subdirectory in the current directory. The CLI handles existing and new projects differently:

* **Existing projects** (local `index.ts` found): Uses smart comparison to merge changes
* **New projects** (no local directory): Generates files from scratch

### Force a full regeneration

To regenerate all files from scratch, ignoring local changes:

```bash
inkeep pull --introspect
```

This bypasses the smart comparison and rebuilds every TypeScript file from the remote configuration. Use this when your local files are in a broken state or you want a clean slate that matches the remote instance exactly.

<Note>
  The `--introspect` flag overwrites all local files. Commit or back up any local changes you want to keep before using it.
</Note>

### Pull for a specific environment

Generate environment-specific credential files by passing the `--env` flag:

```bash
inkeep pull --env production
```

This generates credential configurations in `environments/production.env.ts` instead of the default `environments/development.env.ts`.

### Export as JSON

To inspect the raw project data without generating TypeScript files:

```bash
inkeep pull --json
```

This outputs the project configuration as JSON, which is useful for debugging or piping into other tools.

## What's next

<Cards>
  <Card title="Push to Remote" icon="LuCloudUpload" href="/guides/cli/push-to-remote">
    Push local changes back to remote instance
  </Card>

  <Card title="CLI Reference" icon="LuTerminal" href="/typescript-sdk/cli-reference">
    Full reference for all CLI commands and options
  </Card>
</Cards>


# Push to remote Inkeep instance (/guides/cli/push-to-remote)

Push your local agent configurations to remote Inkeep instance



This tutorial walks you through pushing agent configurations to a remote Inkeep instance.

## Prerequisites

* Access to a remote Inkeep instance (e.g. [Inkeep Enterprise](https://inkeep.com/enterprise?cta_id=docs_nav) or a [self-hosted deployment](/deployment/vercel))
* The Inkeep CLI installed globally:

```bash
npm install -g @inkeep/agents-cli
```

* A CLI profile configured and authenticated (only needed for remote deployments). If you haven't done this yet, follow the [Set up a CLI profile](/guides/cli/setup-profile) tutorial.

## Step 1: Navigate to your project

Navigate to your project directory. A project directory contains an `index.ts` file that exports a project definition:

```bash
cd my-project
```

<Note>
  Run `inkeep push` from the directory that contains your `inkeep.config.ts`, or from any subdirectory below it.
</Note>

## Step 2: Push to remote Inkeep instance

Run the push command:

```bash
inkeep push
```

The CLI will:

1. Detect the project from your `index.ts` file
2. Resolve configuration — your active [CLI profile](/guides/cli/setup-profile) overrides `inkeep.config.ts` for API URLs, API key, and tenant ID (see [Configuration Priority](/typescript-sdk/cli-reference#configuration-priority))
3. Check for conflicts between your local state and remote
4. Push all agents, tools, and configurations to remote Inkeep instance

### Handling conflicts

If the remote instance has changed since your last pull, the CLI detects conflicts and aborts the push.

To resolve conflicts, run `inkeep pull` first — the [interactive merge UI](/guides/cli/pull-from-remote#step-3-resolve-merge-conflicts) walks you through each conflict. Then push again.

To skip conflict detection and overwrite the remote state:

```bash
inkeep push --force
```

## Step 3: Verify the push

After a successful push, the CLI prints a deployment summary with resource counts. You can also verify by opening the [Visual Builder](/visual-builder/sub-agents) to see your updated configurations.

## Push all projects at once

If you have multiple projects in a workspace, push them all:

```bash
inkeep push --all
```

## What's next

<Cards>
  <Card title="Pull from Remote" icon="LuCloudDownload" href="/guides/cli/pull-from-remote">
    Pull agent configurations from remote Inkeep instance to your local project
  </Card>

  <Card title="CLI Reference" icon="LuTerminal" href="/typescript-sdk/cli-reference">
    Full reference for all CLI commands and options
  </Card>
</Cards>


# Set up a CLI profile (/guides/cli/setup-profile)

Create a CLI profile and authenticate with remote Inkeep instance



<Note>
  Profile setup is only required when pushing or pulling to a remote deployment (e.g. [Inkeep Enterprise](https://inkeep.com/enterprise?cta_id=docs_nav) or a [self-hosted deployment](/deployment/vercel)). If you're pushing or pulling locally, you can skip this step.
</Note>

A CLI profile tells the CLI where to connect and how to authenticate with a remote deployment. This tutorial walks you through creating a profile and logging in.

## Prerequisites

* Access to a remote Inkeep instance (e.g. [Inkeep Enterprise](https://inkeep.com/enterprise?cta_id=docs_nav) or a [self-hosted deployment](/deployment/vercel))
* The Inkeep CLI installed globally:

```bash
npm install -g @inkeep/agents-cli
```

## Step 1: Create a profile

Profiles let you manage multiple environments (remote, local, staging) from the same CLI. Create a profile for remote Inkeep instance:

```bash
inkeep profile add <profile-name>
```

The CLI prompts you to configure the profile:

1. **Remote type**:
   * Select **Inkeep Cloud** if you have an Inkeep Enterprise account (URLs are configured automatically)
   * Select **Local** for local development (localhost defaults, no authentication required)
   * Select **Custom** for self-hosted deployments, then enter your API and Manage UI URLs
2. **Environment name**: Enter `production` (or your preferred name)
3. **Credential reference**: Enter a name for your credential (defaults to `inkeep-<profile-name>`)

<Tip>
  You can verify the profile was created by running `inkeep profile list`.
</Tip>

## Step 2: Set the active profile

Set the profile you created as the active profile:

```bash
inkeep profile use <profile-name>
```

Confirm which profile is active:

```bash
inkeep profile current
```

## Step 3: Log in

Authenticate with your active profile:

```bash
inkeep login
```

The CLI will:

1. Open your browser to the Inkeep authentication page
2. Display a device code (e.g., `ABCD-1234`)
3. Wait for you to complete authentication in the browser

Once authenticated, you'll see confirmation:

```
✓ Logged in as you@example.com
✓ Organization: Your Organization
✓ Profile: cloud
```

<Note>
  Your credentials are securely stored in your system keychain. You only need to log in once per profile.
</Note>

## Managing multiple profiles

You can create profiles for different environments and switch between them:

```bash
inkeep profile add staging
inkeep profile use staging
inkeep login
```

To authenticate a specific profile without switching:

```bash
inkeep login --profile staging
```

To remove a profile you no longer need:

```bash
inkeep profile remove staging
```

For a full list of profile commands, see the [CLI Reference](/typescript-sdk/cli-reference).

## What's next

<Cards>
  <Card title="Push to Remote" icon="LuCloudUpload" href="/guides/cli/push-to-remote">
    Push local agent configurations to remote Inkeep instance
  </Card>

  <Card title="Pull from Remote" icon="LuCloudDownload" href="/guides/cli/pull-from-remote">
    Pull agent configurations from remote Inkeep instance to your local project
  </Card>
</Cards>


# CLI Observability with Langfuse (/guides/observability/cli-observability)

Track LLM operations during pull command with Langfuse



The Inkeep CLI includes built-in observability for tracking LLM operations during the `pull` command. This allows you to monitor costs, latency, and quality of AI-generated code across different LLM providers.

## Overview

When you run `inkeep pull`, the CLI uses LLMs to generate TypeScript files for your agents, tools, and components. With Langfuse integration enabled, you can:

* **Track token usage and costs** across Anthropic, OpenAI, and Google models
* **Monitor generation latency** to identify slow operations
* **View complete traces** of multi-file code generation
* **Analyze placeholder optimization** impact on token savings
* **Debug failed generations** with full context

## Setup

### 1. Create a Langfuse Account

Sign up for a free account at [cloud.langfuse.com](https://cloud.langfuse.com) (EU region) or [us.cloud.langfuse.com](https://us.cloud.langfuse.com) (US region).

### 2. Get API Keys

From your Langfuse dashboard:

1. Navigate to **Settings** → **API Keys**
2. Create a new API key pair
3. Copy both the **Secret Key** (`sk-lf-...`) and **Public Key** (`pk-lf-...`)

### 3. Configure Environment Variables

Add these variables to your `.env` file in your project root:

```bash
# Enable Langfuse tracing
LANGFUSE_ENABLED=true

# Your Langfuse credentials
LANGFUSE_SECRET_KEY=sk-lf-your-secret-key-here
LANGFUSE_PUBLIC_KEY=pk-lf-your-public-key-here

# Langfuse API URL (defaults to EU cloud)
LANGFUSE_BASEURL=https://cloud.langfuse.com  # or https://us.cloud.langfuse.com for US

# Your LLM provider API keys (at least one required)
ANTHROPIC_API_KEY=your-anthropic-key
OPENAI_API_KEY=your-openai-key
GOOGLE_GENERATIVE_AI_API_KEY=your-google-key
```

### 4. Run Pull Command

Now when you run `inkeep pull`, all LLM operations will be traced to Langfuse:

```bash
inkeep pull --project my-agent-project
```

## Viewing Traces

### In Langfuse Dashboard

1. Go to your [Langfuse dashboard](https://cloud.langfuse.com)
2. Navigate to **Traces**
3. You'll see traces for each file generation operation

### Trace Metadata

Each trace includes rich metadata:

| Field              | Description                  | Example                           |
| ------------------ | ---------------------------- | --------------------------------- |
| `fileType`         | Type of file being generated | `agent`, `tool`, `data_component` |
| `placeholderCount` | Number of placeholders used  | `5`                               |
| `promptSize`       | Size of prompt in characters | `15234`                           |
| `model`            | LLM model used               | `claude-sonnet-4-5`               |

### Example Trace Structure

```
Service: inkeep-agents-cli
├── generate-agent-file
│   ├── Model: claude-sonnet-4-5
│   ├── Tokens: 12,543 input / 3,421 output
│   ├── Duration: 8.3s
│   └── Metadata:
│       ├── fileType: agent
│       ├── placeholderCount: 12
│       └── promptSize: 25,891 chars
```

## Monitoring Strategies

### Track Costs by Provider

Compare costs across different LLM providers:

1. Filter traces by model in Langfuse
2. View cumulative costs in the **Usage** dashboard
3. Identify cost-saving opportunities

### Optimize Generation Time

Find slow generation steps:

1. Sort traces by duration
2. Check if complex agents need longer timeouts
3. Consider using faster models for simpler files

### Analyze Token Savings

Monitor placeholder optimization impact:

1. Look at `placeholderCount` metadata
2. Higher counts = more token savings
3. Useful for understanding efficiency gains

## Troubleshooting

### Traces Not Appearing

**Check if Langfuse is enabled:**

```bash
# Should output: true
echo $LANGFUSE_ENABLED
```

**Verify API keys are set:**

```bash
# Should show your keys (without exposing them)
env | grep LANGFUSE
```

**Check for errors:**

```bash
# Run with debug logging
DEBUG=* inkeep pull --project my-project
```

### Missing Metadata

If traces appear but lack metadata:

1. Ensure you're using the latest CLI version
2. Check that file type context is being passed correctly
3. Report issues on [GitHub](https://github.com/anthropics/claude-code/issues)

## Privacy Considerations

### What Data is Sent to Langfuse

* **Prompt content**: The full prompts sent to LLMs (includes your project data)
* **Generated code**: The TypeScript code generated by LLMs
* **Model metadata**: Model names, token counts, timings
* **File metadata**: File types, sizes, placeholder counts

### What is NOT Sent

* **Your API keys**: LLM provider keys are never sent to Langfuse
* **Other environment variables**: Only Langfuse-specific vars are used

### Self-Hosted Option

For complete control over your data, you can self-host Langfuse:

```bash
# Use your self-hosted instance
LANGFUSE_BASEURL=https://langfuse.your-domain.com
```

See [Langfuse self-hosting docs](https://langfuse.com/docs/deployment/self-host) for details.

## Best Practices

1. **Enable for development**: Keep tracing on during development to catch issues early
2. **Disable in CI/CD**: Turn off for automated builds to avoid unnecessary traces
3. **Review weekly**: Check Langfuse dashboard weekly to monitor costs and performance
4. **Set budgets**: Configure spending alerts in your LLM provider dashboards

## Related Documentation

* [SignOz Usage](/guides/observability/signoz-usage) - OpenTelemetry tracing for runtime operations
* [Langfuse Usage](/guides/observability/langfuse-usage) - Langfuse integration for agent runtime
* [CLI Reference](/typescript-sdk/cli-reference) - Complete CLI command reference


# Using Langfuse for LLM Observability (/guides/observability/langfuse-usage)

Complete guide to using Langfuse for LLM observability, tracing, and analytics in the Inkeep Agent Framework



Langfuse is an open-source LLM engineering platform that provides specialized observability for AI applications, including token usage tracking, model performance analytics, and detailed LLM interaction tracing.

## Quick Start

### 1. Setup Langfuse Account

First, create a Langfuse account and get your API keys:

1. **Sign up** at [Langfuse Cloud](https://cloud.langfuse.com)
2. **Create a new project** in your Langfuse dashboard
3. **Get your API keys** from the project settings:
   * Public Key: `pk-lf-xxxxxxxxxx`
   * Secret Key: `sk-lf-xxxxxxxxxx`

### 2. Configure Langfuse

To integrate Langfuse with your Inkeep Agent Framework instrumentation, you need to modify your instrumentation file to include the Langfuse span processor.

Replace the default setup with a custom NodeSDK configuration:

Set your environment variables:

```bash
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxxxx
LANGFUSE_BASE_URL=https://us.cloud.langfuse.com
```

Update your instrumentation file:

```typescript
import {
  defaultSpanProcessors,
  defaultContextManager,
  defaultResource,
  defaultTextMapPropagator,
  defaultInstrumentations,
} from "@inkeep/agents-api/instrumentation";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";

export const defaultSDK = new NodeSDK({
  resource: defaultResource,
  contextManager: defaultContextManager,
  textMapPropagator: defaultTextMapPropagator,
  spanProcessors: [...defaultSpanProcessors, new LangfuseSpanProcessor()],
  instrumentations: defaultInstrumentations,
});

defaultSDK.start();
```

<Note>
  Make sure to install the required dependencies in your agents API directory:
  `bash cd apps/agents-api pnpm add @opentelemetry/sdk-node @langfuse/otel `
</Note>

#### What This Configuration Does

* **Preserves all default instrumentation**: Uses the same resource, context manager, propagator, and instrumentations as the default setup
* **Adds Langfuse span processor**: Extends the default span processors with Langfuse's processor for specialized LLM observability
* **Maintains compatibility**: Your existing traces will continue to work while adding Langfuse-specific features

## Dataset setup and execution

Use the [Inkeep Agent Cookbook](https://github.com/inkeep/agents/tree/main/agents-cookbook) which provides ready-to-use scripts for creating and running Langfuse dataset evaluations programmatically.

#### 1. Clone the Agents Repository

```bash
git clone https://github.com/inkeep/agents.git
cd agents/agents-cookbook/evals/langfuse-dataset-example
```

**Set up environment variables in a `.env` file:**

```bash
# Langfuse configuration (required for both scripts)
LANGFUSE_PUBLIC_KEY=your_langfuse_public_key
LANGFUSE_SECRET_KEY=your_langfuse_secret_key
LANGFUSE_BASE_URL=https://cloud.langfuse.com

# Chat API configuration (for dataset runner)
INKEEP_AGENTS_RUN_API_KEY=your_api_key
INKEEP_AGENTS_RUN_API_URL=your_chat_api_base_url

# Execution context (for dataset runner)
INKEEP_TENANT_ID=your_tenant_id
INKEEP_PROJECT_ID=your_project_id
INKEEP_AGENT_ID=your_agent_id
```

#### 2. Initialize Dataset with Sample Data

Run the basic Langfuse example to initialize a dataset with sample user messages:

```bash
pnpm run langfuse-init-example
```

This script will:

* Connect to your Langfuse project
* Create a new dataset called "inkeep-weather-example-dataset" with sample dataset items

#### 3. Run Dataset Items to Generate Traces

Run dataset items to generate traces that can be evaluated:

```bash
pnpm run langfuse-run-dataset
```

This script will:

* Read items from your Langfuse dataset
* Execute each item against your weather agent
* Generate the data needed for evaluation

## Running LLM Evaluations in Langfuse Dashboard

Langfuse provides a powerful web interface for running LLM evaluations without writing code. You can create datasets, set up evaluators, and run evaluations directly in the dashboard.

### Accessing the Evaluation Features

1. **Log into your Langfuse dashboard**: [https://cloud.langfuse.com](https://cloud.langfuse.com)
2. **Navigate to your project** where your agent traces are being collected
3. **Click "Evaluations"** in the left sidebar
4. **Click "Set up evaluator"** to begin creating evaluations

### Setting Up LLM-as-a-Judge Evaluators

#### Set Up Default Evaluation Model

Before creating evaluators, you need to configure a default LLM connection for evaluations:

<Image src="/images/langfuse-llm-connection.png" alt="Langfuse LLM Connection setup showing OpenAI provider configuration with API key field and advanced settings" style={{ border: "1px solid #e1e5e9" }} />

**Setting up the LLM Connection:**

1. **Navigate to "Evaluator Library"** in your Langfuse dashboard
2. **Click "Set up"** next to "Default Evaluation Model"
3. **Configure the LLM connection**:
   * **LLM Adapter**: Select your preferred provider
   * **Provider Name**: Give it a descriptive name (e.g., "openai")
   * **API Key**: Enter your OpenAI API key (stored encrypted)
   * **Advanced Settings**: Configure base URL, model parameters if needed
4. **Click "Create connection"** to save

#### Navigate to Evaluator Setup

1. **Go to "Evaluations"** → **"Running Evaluators"**
2. **Click "Set up evaluator"** button
3. **You'll see two main steps**: "1. Select Evaluator" and "2. Run Evaluator"

#### Choose Your Evaluator Type

You have two main options:

## Option A: Langfuse Managed Evaluators

Langfuse provides a comprehensive catalog of **pre-built evaluators**

**To use a managed evaluator:**

1. **Browse the evaluator list** and find one that matches your needs
2. **Click on the evaluator** to see its description and criteria
3. **Click "Use Selected Evaluator"** button

#### Customizing Managed Evaluators for Dataset Runs

Once you've selected a managed evaluator, you can **edit it to target your dataset runs**. This is particularly useful for evaluating agent performance against known test cases.

### Example: Customizing a Helpfulness Evaluator

1. **Select the "Helpfulness" evaluator** from the managed list
2. Under **Target** select dataset runs
3. **Configure variable mapping**
   * **{`{{input}}`}** → **Object**: Trace, **Object Variable**: Input
   * **{`{{generation}}`}** → **Object**: Trace, **Object Variable**: Output

<Image
  src="/images/langfuse-helpfulness-setup.png"
  style={{
  border: "1px solid #e1e5e9",
  maxWidth: "1600px",
  height: "auto",
  minHeight: "300px",
}}
/>

## Option B: Create Custom Evaluator

1. **Click "+ Create Custom Evaluator"** button
2. **Fill in evaluator details**:

   * **Name**: Choose a descriptive name (e.g., "weather\_tool\_used")
   * **Description**: Explain what this evaluator measures
   * **Model**: Select evaluation model
   * **Prompt**: Configure a custom prompt

### Example: Customizing a Weather Tool Evaluator

1. **Prompt**

```
You are an expert evaluator for an AI agent system.
Your task is to rate the correctness of tool usage on a scale from 0.0 to 1.0.

Instructions:

If the user’s question is not weather-related and the tool used is not get_weather_forecast, return 1.0.

If the user’s question is not weather-related and the tool is get_weather_forecast, return 0.0.

If the user’s question is weather-related, return 1.0 only if the tool used is get_weather_forecast; otherwise return 0.0.

Input:
User Question: {`{{input}}`}
Tool Used: {`{{tool_used}}`}
```

<Image
  src="/images/custom-eval.png"
  style={{
  width: "80%",
  border: "1px solid #e1e5e9",
  maxWidth: "800px",
  height: "auto",
}}
/>

2. **Configure variable mapping**:
   * **{`{{input}}`}** → **Object**: Trace, **Object Variable**: Input
   * **{`{{tool_used}}`}** → **Object**: Span, **Object Name**: weather-forecaster.ai.toolCall, **Object Variable**: Metadata, **JsonPath**: $.attributes\["ai.toolCall.name"]

<Image
  src="/images/custom-variable-mapping.png"
  alt="Langfuse helpfulness evaluator setup screen showing evaluator configuration with variable mapping and trace targeting options"
  style={{
  border: "1px solid #e1e5e9",
  maxWidth: "1200px",
  height: "auto",
  minHeight: "300px",
}}
/>

## Enable and Monitor

1. **Click "Enable Evaluator"** to start automatic evaluation
2. **Monitor evaluation progress** in the dashboard
3. **View evaluation results** as they complete


# Prompt Caching (/guides/observability/prompt-caching)

How Inkeep caches Anthropic prompt prefixes by default, how to observe cache behavior, and how to control it



Inkeep enables Anthropic prompt caching by default for the main agent generation call. Caching lets the provider reuse a previously sent prompt prefix (the tool definitions plus the system prompt) instead of reprocessing it on every turn, so repeated input tokens are billed at the provider's reduced cache-read rate. No configuration is required — caching is on out of the box, and its effect is visible on the conversation trace timeline and the cost dashboard.

<Note>
  Inkeep currently attaches caching only to the **main agent generation** call. Other call sites (status updates, distillation, artifact metadata) get whatever automatic caching the provider already offers. Savings depend on workload: prefixes that stay stable across turns within the cache window are reused, while per-turn unique content (such as retrieval payloads) is never cacheable.
</Note>

## How it works

Caching attaches at the model call based on how the agent routes to Anthropic:

* **Gateway mode** (routing through the Vercel AI Gateway): Inkeep sets `providerOptions.gateway.caching: 'auto'`. The gateway places a single cache marker covering the `tools` + `system` wire prefix.
* **Direct mode** (routing directly to Anthropic): Inkeep sets `providerOptions.anthropic.cacheControl: { type: 'ephemeral' }` on the **system message**, which caches the same `tools` + `system` prefix.

Both modes use Anthropic's default 5-minute ephemeral cache. The first turn writes the prefix to the cache (a cache-creation cost), and subsequent turns within the window read it back at the reduced rate.

<Note>
  Caching only applies above the provider's minimum cacheable prefix size (1,024 tokens for Claude Sonnet 4.5/4.6; 4,096 tokens for Claude Haiku 4.5 and Opus 4.5+). Calls below that floor are simply not cached — eligibility is delegated to the provider, not enforced by Inkeep.
</Note>

## Cache states

Every LLM generation span carries cache telemetry, from which Inkeep derives one of five cache states per call:

| State                         | Meaning                                                                                                                                                |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **HIT**                       | Caching was attempted and the provider served tokens from cache — the win condition.                                                                   |
| **MISS-expected**             | Caching was attempted but the prompt prefix changed since the last call, so no cache read happened. Normal on the first turn or after a prefix change. |
| **MISS-regression**           | Caching was attempted and the prefix was unchanged, yet nothing was read from cache. The actionable alarm state — something broke cross-turn caching.  |
| **NOT-ATTEMPTED**             | No cache markers were attached (for example, the prefix was below the provider floor, or caching is disabled).                                         |
| **NOT-SUPPORTED-BY-PROVIDER** | The routed provider does not support marker-based caching.                                                                                             |

## Observe cache behavior

### Conversation trace timeline

In the Manage UI, open a conversation's trace timeline. Each LLM call row shows a cache-state badge next to its per-call cost. The badge encodes state through an icon, a label, and a color variant (not color alone):

* **HIT** renders as a success badge
* **MISS-regression** renders as an error badge — the one state worth investigating
* **MISS-expected** renders as a warning badge
* **NOT-ATTEMPTED** and **NOT-SUPPORTED-BY-PROVIDER** share a neutral badge, distinguished by label and tooltip

### Cost dashboard

The cost dashboard includes a **Cost by Cache Participation** breakdown table alongside the existing Cost by Model, Agent, Provider, and Generation Type tables. It groups spend into cached, cache-write, and uncached buckets so you can see how much of your input cost benefited from caching. A **Cache-read Tokens** stat card summarizes cache-read volume across the selected time range.

## Control caching

### Disable globally with an environment variable

Set `INKEEP_PROMPT_CACHING_ENABLED` to turn Inkeep-attached caching off across the deployment — useful for A/B testing caching versus no caching, or for self-hosters who want to opt out.

| Variable                        | Default | Description                                                                                                                                          |
| ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INKEEP_PROMPT_CACHING_ENABLED` | `true`  | Set to `false` to stop Inkeep from attaching any caching options. Customer-provided `providerOptions` still pass through to the model SDK unchanged. |

```dotenv title=".env"
# Disable Inkeep-attached prompt caching for the whole deployment
INKEEP_PROMPT_CACHING_ENABLED=false
```

<Note>
  This variable is parsed as a boolean (Zod's `stringbool`). Caching is **disabled** by any case-insensitive falsy value: `false`, `0`, `no`, `off`, `n`, or `disabled`. Truthy values (`true`, `1`, `yes`, `on`, `y`, `enabled`) or leaving it unset keep caching **enabled**, and any other value is rejected at startup.
</Note>

### Override per agent with providerOptions

Inkeep attaches its caching default only when an agent has not set one itself: `providerOptions.gateway.caching` in gateway mode, or per-message `providerOptions.anthropic.cacheControl` on system messages in direct (Anthropic) mode. If an agent supplies its own non-null value, Inkeep preserves it instead of overriding it.

<Warning>
  There is no clean per-agent **off** switch today, in either gateway or direct mode. In direct mode, setting `cacheControl: null` in an agent's model config does not disable caching: Inkeep attaches `cacheControl` to system messages, and a null value falls back to the default regardless. The gateway's `caching` option likewise has no per-agent off value. To disable Inkeep-attached caching, use the `INKEEP_PROMPT_CACHING_ENABLED` deployment kill switch. A productized per-agent caching control is planned for a future release.
</Warning>

## Scope and limitations

* **In scope:** default-on caching at the main agent generation call (gateway and direct mode), provider-aware cache telemetry, and the Manage UI surfaces above.
* **Not in scope yet:** a per-agent or per-tenant caching configuration UI, a customer-facing cost estimator, and explicit Gemini context caching. These are planned for a future release.

## Related

* [Using SigNoz for Observability](/guides/observability/signoz-usage)
* [Model Configuration](/typescript-sdk/models)


# Using SigNoz for Observability (/guides/observability/signoz-usage)

Complete guide to using SigNoz for observability, monitoring, tracing in the Inkeep Agent Framework



SigNoz is a full-stack observability platform that provides distributed tracing so that you can track requests across multiple agents and services.

### Quick Start

Before using SigNoz, ensure it's properly set up and running. For setup instructions, see the [Quick Start](/get-started/traces) guide.

## Using SigNoz UI

The **Traces** page provides detailed request tracing:

#### Viewing Traces

The SigNoz traces interface provides comprehensive visibility into your agent operations:

<Image src="/images/signoz-traces-ui.png" alt="SigNoz Traces Explorer showing inkeep-agents-api service traces with timestamp, service name, operation name, duration, HTTP method, and response status code columns" style={{ border: "1px solid #e1e5e9" }} />

The traces explorer shows:

* **Timestamp**: When each span occurred
* **Service Name**: The service that generated the span (e.g., `inkeep-agents-api`)
* **Operation Name**: Specific operations like `ai.generateText`, `tls.connect`, `ai.toolCall`
* **Duration**: How long each operation took (in milliseconds)
* **HTTP Method**: For HTTP operations, shows the method (POST, GET, etc.)
* **Response Status Code**: HTTP status codes (200, 404, etc.)

Key features of the traces view:

* **Filtering Options**: Use the left sidebar to filter by duration, deployment environment, service name, and more

* **Time Range Selection**: Choose from preset ranges or custom time periods

* **Multiple Views**: Switch between List View, Traces, Time Series, and Table View

* **Real-time Updates**: Traces refresh automatically to show new data

* **Trace List**: Browse all traces with filtering options

* **Trace Details**: Drill down into individual traces

* **Span Timeline**: See the execution flow across agents

#### Filtering Traces

```
# Filter by service
service_name = "inkeep-agents-api"

# Filter by operation
operation = "agent.generate"

# Filter by status
status = "error"

# Filter by duration
duration > 1000ms

# Filter by custom attributes
agent.id = "customer-support-agent"
```

#### Analyzing Individual Traces

When you click on a specific trace from the list, you'll see the detailed trace view with a flamegraph visualization:

<Image src="/images/signoz-trace-details.png" alt="SigNoz Trace Details showing flamegraph visualization with span hierarchy, timing information, and detailed span attributes" style={{ border: "1px solid #e1e5e9" }} />

**Flamegraph Visualization:**

* **Horizontal Bars**: Each bar represents a span (operation) in your trace
* **Bar Width**: Proportional to the duration of the operation
* **Color Coding**:
  * Blue bars: Successful operations
  * Red bars: Operations with errors

**Key Information Displayed:**

* **Total Spans**: Total number of operations in this trace (e.g., 122)
* **Error Spans**: Number of spans that encountered errors (e.g., 19)
* **Trace Duration**: Total time for the entire trace (e.g., 5.2 mins)
* **Timestamp**: When the trace occurred
* **Service**: The primary service (e.g., `inkeep-agents-api`)

**Span Details Panel (Right Side):**

* **Span Name & ID**: Operation name and unique identifier
* **Timing**: Start time and duration
* **Service & Kind**: Which service and span type (Server, Client, etc.)
* **Status**: Success/error status code
* **Attributes, Events & Links**: Additional span metadata

**How to Use This View:**

1. **Identify Bottlenecks**: Look for the widest bars in the flamegraph - these represent the longest-running operations
2. **Find Errors**: Red bars indicate operations that failed - click on them to see error details
3. **Understand Flow**: Follow the vertical hierarchy to see how operations call each other
4. **Analyze Performance**: Use the timeline to see which operations run in parallel vs. sequentially
5. **Drill Down**: Click on any span to see detailed attributes, events, and error information

## Cache Observability

Every LLM generation span includes prompt-cache telemetry. In the Manage UI:

* The **conversation trace timeline** shows a per-call cache-state badge (HIT / MISS-regression / MISS-expected / NOT-ATTEMPTED) next to each call's cost.
* The **cost dashboard** includes a **Cost by Cache Participation** breakdown alongside the existing Cost by Model, Agent, Provider, and Generation Type tables.

You can also query raw cache attributes directly in SigNoz using the span attributes emitted by the gateway cost middleware:

```
gen_ai.usage.cache_read.input_tokens > 0
gen_ai.usage.cache_creation.input_tokens > 0
cache.intent.marker_count > 0
```

See [Prompt Caching](/guides/observability/prompt-caching) for the full cache-state taxonomy and how to interpret MISS-regression signals.


# App Credentials (/talk-to-your-agents/app-credentials)

How app credentials authenticate end-users, including anonymous sessions, authenticated chat sessions with asymmetric key verification, domain validation, and Proof-of-Work protection.



App credentials are the auth mechanism for [chat components](/talk-to-your-agents). This page covers the underlying auth flow, security model, and advanced configuration.

Existing API keys will continue to work for server-to-server integrations. Use App Credentials for new app-based integrations such as web clients, support copilots, and authenticated end-user experiences.

## How anonymous authentication works

When your widget loads, it authenticates through a two-step flow:

1. **Get a session token** — the widget calls `POST /run/auth/apps/{appId}/anonymous-session`. The server validates the request `Origin` against the app's allowed domains and returns a JWT.
2. **Use the token** — subsequent chat requests include the JWT and App ID:
   ```http
   Authorization: Bearer <session_token>
   X-Inkeep-App-Id: <app_id>
   ```

Each session gets a unique anonymous user ID (`anon_<uuid>`), enabling per-user conversation history.

To **refresh a token** while preserving the same anonymous identity, include the existing token as an `Authorization: Bearer` header when requesting a new session. If the token is valid, the server reuses the same user ID (`sub` claim) and issues a fresh token with a new 30-day expiry. If the token is invalid, expired, or belongs to a different app, a new anonymous identity is created as usual.

## Authenticated chat sessions

In addition to anonymous sessions, apps can be configured for **authenticated chat sessions**. Your backend signs JWTs with a private key, and Inkeep verifies them using the corresponding public key you upload. This gives each session a verified user identity tied to your system's user IDs.

### How it works

<Steps>
  <Step>
    **Generate a key pair** — create an asymmetric key pair (RSA, EC, or EdDSA) on your infrastructure.
  </Step>

  <Step>
    **Upload the public key** — register the public key with your app via the API or Visual Builder. Inkeep never sees your private key.
  </Step>

  <Step>
    **Sign JWTs on your backend** — when a user starts a chat session, your server signs a JWT containing the user's identity.

    ```typescript title="routes/inkeep-token.ts"
    import { Router } from 'express';
    import { SignJWT, importPKCS8 } from 'jose';
    import { auth } from '../lib/auth'; // your auth library

    const router = Router();

    const PRIVATE_KEY_PEM = process.env.INKEEP_SIGNING_KEY!;
    const KEY_ID = process.env.INKEEP_KEY_ID!; // matches the kid uploaded to Inkeep

    router.post('/api/inkeep-token', async (req, res) => {
      // Verify the user's session with your auth system
      const session = await auth.api.getSession();

      if (!session) {
        return res.status(401).json({ error: 'Not authenticated' });
      }

      const now = Math.floor(Date.now() / 1000);
      const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'RS256');

      const token = await new SignJWT({
        sub: session.user.id,      // becomes the userId in Inkeep
        email: session.user.email, // optional — available as a verified claim
      })
        .setProtectedHeader({ alg: 'RS256', kid: KEY_ID })
        .setIssuedAt(now)
        .setExpirationTime(now + 3600) // 1 hour
        .sign(privateKey);

      return res.json({ token });
    });

    export default router;
    ```
  </Step>

  <Step>
    **Widget sends the signed JWT** — pass a `getAuthToken` function in `baseSettings` that fetches a token from your backend route. The widget calls it automatically and includes the JWT as a `Bearer` token instead of requesting an anonymous session.

    <CodeGroup>
      ```typescript title="React"
      import { InkeepEmbeddedChat, type InkeepBaseSettings, type InkeepAiChatSettings } from '@inkeep/agents-ui-cloud';

      async function getAuthToken(): Promise<string> {
        const res = await fetch('/api/inkeep-token', { method: 'POST' });
        if (!res.ok) throw new Error('Failed to get auth token');
        const { token } = await res.json();
        return token;
      }

      export function AuthenticatedChat() {
        const aiChatSettings: InkeepAiChatSettings = {
          appId: 'YOUR_APP_ID',
        };
        const baseSettings: InkeepBaseSettings = {
          getAuthToken,
        };

        return <InkeepEmbeddedChat baseSettings={baseSettings} aiChatSettings={aiChatSettings} />;
      }
      ```

      ```javascript title="JavaScript"
      async function getAuthToken() {
        const res = await fetch('/api/inkeep-token', { method: 'POST' });
        if (!res.ok) throw new Error('Failed to get auth token');
        const { token } = await res.json();
        return token;
      }

      Inkeep.EmbeddedChat('#chat-target', {
        baseSettings: {
          getAuthToken,
        },
        aiChatSettings: {
          appId: 'YOUR_APP_ID',
        },
      });
      ```
    </CodeGroup>
  </Step>

  <Step>
    **Inkeep verifies the signature** — the server matches the `kid` header to a stored public key, verifies the signature, and extracts the user identity from the `sub` claim.
  </Step>
</Steps>

### JWT requirements

| Field | Location | Required | Description                                              |
| ----- | -------- | -------- | -------------------------------------------------------- |
| `kid` | Header   | Yes      | Must match the `kid` of an uploaded public key           |
| `sub` | Payload  | Yes      | User identifier — becomes the `userId` for the session   |
| `exp` | Payload  | Yes      | Expiration time, max 24 hours from `iat`                 |
| `iat` | Payload  | Yes      | Issued-at time, must be within 60 seconds of server time |

### Supported algorithms

| Family    | Algorithms          |
| --------- | ------------------- |
| **RSA**   | RS256, RS384, RS512 |
| **ECDSA** | ES256, ES384, ES512 |
| **EdDSA** | EdDSA (Ed25519)     |

<Note>
  RSA keys must be at least 2048 bits. Private keys are rejected at upload time — only public keys are accepted.
</Note>

### Enforcing authentication

New apps **require authenticated sessions by default**. If a signed JWT is missing or verification fails, the request is rejected with a `401 Unauthorized` response.

To allow anonymous sessions (e.g., for public-facing widgets that don't require user identity), disable **Require Authentication** in the app edit dialog. When disabled, requests without a valid JWT fall back to anonymous authentication instead of being rejected.

### Verified claims

Non-standard claims in the signed JWT (beyond `sub`, `iat`, `exp`, `aud`, `iss`, `jti`, `nbf`) are extracted and made available as **verified claims** in the conversation context. These are cryptographically signed and kept separate from unverified `userProperties` sent by the client.

<Warning>
  Verified claims are limited to 1KB. Tokens with custom claims exceeding this limit are rejected.
</Warning>

### Key management

Manage public keys from the app edit dialog in the Visual Builder. Navigate to **Apps** → click the **⋯** menu on your Web Client app → **Edit**, then scroll to the **Authentication Keys** section.

From here you can add, view, copy, and delete public keys. Each key requires:

| Field                | Description                                                                                          |
| -------------------- | ---------------------------------------------------------------------------------------------------- |
| **Key ID (kid)**     | A unique identifier for this key (e.g. `my-key-1`). Must match the `kid` header in your signed JWTs. |
| **Algorithm**        | Select from the available algorithms listed in the UI. Must match your key pair.                     |
| **Public Key (PEM)** | The PEM-encoded public key, starting with `-----BEGIN PUBLIC KEY-----`.                              |

<Tip>
  You can register multiple public keys per app. Use this for key rotation — upload a new key before retiring the old one.
</Tip>

Below the keys section, the **Audience (aud)** field lets you require that signed JWTs include a matching `aud` claim. When set, tokens without a matching audience are rejected. See [Enforcing authentication](#enforcing-authentication) for the **Require Authentication** toggle.

## Security Model

| Feature                      | Details                                                                                                    |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **Domain allowlist**         | `Origin` header validated against the app's `allowedDomains` at token issuance                             |
| **Scoped access**            | Each app is bound to a default agent via `defaultAgentId`                                                  |
| **Captcha**                  | Proof-of-Work challenges prevent automated abuse                                                           |
| **Anonymous identities**     | Each anonymous session gets a unique user ID for per-user conversation history                             |
| **Authenticated identities** | User ID from verified `sub` claim when using signed JWTs                                                   |
| **Auth enforcement**         | New apps require authentication by default; disable **Require Authentication** to allow anonymous sessions |
| **Token expiry**             | Anonymous session tokens default to 30 days; authenticated tokens max 24 hours                             |
| **Rolling refresh**          | Include existing anonymous token when requesting a new session to preserve identity with a fresh expiry    |
| **Verified claims**          | Non-standard JWT claims from authenticated sessions available in conversation context                      |

## App Credentials vs API Keys

|                          | App Credentials                                                                                      | API Keys                      |
| ------------------------ | ---------------------------------------------------------------------------------------------------- | ----------------------------- |
| **Use case**             | App-based integrations such as web clients, support copilots, and authenticated end-user experiences | Server-to-server integrations |
| **Exposed to end-users** | Yes (App ID only)                                                                                    | No (secret)                   |
| **Domain restrictions**  | Yes                                                                                                  | No                            |
| **Per-user identity**    | Yes (anonymous or authenticated)                                                                     | No                            |
| **Default agent**        | One agent (via `defaultAgentId`)                                                                     | One agent per key             |

## Related

* [Chat Components](/talk-to-your-agents) — widget setup and configuration
* [Chat API](/talk-to-your-agents/chat-api) — low-level streaming API details
* [Apps API Reference](/api-reference/apps) — CRUD operations for app management
* [Auth API Reference](/api-reference/auth) — session and PoW challenge endpoints


# Widget Changelog (/talk-to-your-agents/changelog)

Changelog for the @inkeep/agents-ui-cloud package



{/* AUTO-GENERATED — do not edit directly. Updated by the sync-widget-changelog workflow. */}

## 0.15.19

### Patch Changes

* a6744fb: - Don't show chat history sidepane when the support form is open
* bd90ee6: Add an event for when the conversation id changes

## 0.15.18

### Patch Changes

* 9de9de5: Add placeholder message if user loads a chat that was interrupted
  Fix stutter when reopening chat history panel

## 0.15.17

### Patch Changes

* ba52082: - Rolling anon session support


# Chat Components Overview (/talk-to-your-agents)

Pre-built chat components to integrate Inkeep agents into your application



The Inkeep chat components provide pre-built UI elements to quickly integrate conversational AI into your application. These components handle the chat interface, message streaming, and user interactions out of the box.

## Available Components

| Component                                                   | Description                                        |
| ----------------------------------------------------------- | -------------------------------------------------- |
| [Chat Button](/talk-to-your-agents/react/chat-button)       | A floating button that opens a chat modal          |
| [Custom Trigger](/talk-to-your-agents/react/custom-trigger) | Use your own button or element to trigger the chat |
| [Side Bar Chat](/talk-to-your-agents/react/side-bar-chat)   | A chat panel that slides in from the side          |
| [Embedded Chat](/talk-to-your-agents/react/embedded-chat)   | A chat component embedded directly in your page    |

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Framework Support

The chat components are available for:

* **[React](/talk-to-your-agents/react/chat-button)** - Native React components with full TypeScript support
* **[Next.js](/talk-to-your-agents/nextjs/chat-button)** - Same React components with App Router–friendly setup (dynamic import, `"use client"`)
* **[JavaScript](/talk-to-your-agents/javascript/chat-button)** - Vanilla JavaScript for any framework or plain HTML

## Setup

To use chat components, create an **app credential** — this configures domain restrictions, agent access, and end-user identity for your widget.

1. Open the Visual Builder Dashboard
2. Navigate to your Project → **Apps**
3. Click **Create App** and set the type to `web_client`
4. Add your **allowed domains** (e.g., `docs.example.com`)
5. Select a **default agent**
6. Copy your **App ID** (e.g., `app_a1b2c3d4e5f6`) — you'll pass this to the widget

See [App Credentials](/talk-to-your-agents/app-credentials) for the full auth flow details, security model, and API-based setup.

{/* Keep the [#custom-headers] id: this anchor was shared externally before the content moved to the passing-context page. Removing it breaks existing deep links. */}

## Pass context to the widget

Send page, session, and user context to your agent so it can give personalized, situation-aware responses. See [Pass context to the widget](/talk-to-your-agents/customization/passing-context) for automatic headers, custom headers, and user properties.

## Customization

Learn how to customize the appearance and behavior of chat components in the [Styling guide](/talk-to-your-agents/customization/styling).


# Interactive Components (/talk-to-your-agents/interactive-components)

Render clickable components inside AI responses that programmatically send follow-up messages on the user's behalf.



Custom components let your agent render rich, interactive UI inside chat responses. Combined with `chatFunctionsRef` (React) or the widget instance (JavaScript), those components can programmatically submit follow-up messages when a user clicks them, creating guided, conversational workflows without the user needing to type.

## How it works

1. Your agent calls a tool that emits a **component data part** with a `type`, `name`, and `props`
2. The chat widget matches the component name to a render function you provide via the `components` prop
3. Your render function builds the UI and calls `submitMessage()` to send a message on the user's behalf

## Setup

The pattern requires two things in your `aiChatSettings`:

* `components` - a map of component names to render functions
* A way to call chat methods like `submitMessage` from inside those render functions. In React, use a `chatFunctionsRef`. In JavaScript, use the widget instance returned by `Inkeep.EmbeddedChat()`.

<CodeGroup>
  ```tsx title="React"
  import { useRef } from 'react'
  import { InkeepEmbeddedChat } from '@inkeep/agents-ui-cloud'
  import type {
    AIChatFunctions,
    InkeepAIChatSettings,
    ComponentsConfig,
  } from '@inkeep/agents-ui-cloud/types'

  export default function Chat() {
    const chatFunctionsRef = useRef<AIChatFunctions | null>(null)

    const components: ComponentsConfig<{
      QuickActions: { actions: { label: string; message: string }[] }
    }> = {
      QuickActions: (props) => (
        <div className="flex gap-2 flex-wrap my-2">
          {props.actions.map((action) => (
            <button
              key={action.label}
              className="px-3 py-1.5 rounded-lg border border-gray-200 bg-white text-sm cursor-pointer hover:border-indigo-500 transition-colors"
              onClick={() => chatFunctionsRef.current?.submitMessage(action.message)}
            >
              {action.label}
            </button>
          ))}
        </div>
      ),
    }

    const aiChatSettings: InkeepAIChatSettings = {
      appId: process.env.REACT_APP_INKEEP_APP_ID,
      chatFunctionsRef,
      components,
    }

    return <InkeepEmbeddedChat aiChatSettings={aiChatSettings} />
  }
  ```

  ```js title="JavaScript"
  const widget = Inkeep.EmbeddedChat("#ikp-embedded-chat-target", {
    aiChatSettings: {
      appId: "YOUR_APP_ID",
      components: {
        QuickActions: (props, target) => {
          const container = document.createElement("div");
          container.className = "flex gap-2 flex-wrap my-2";

          for (const action of props.actions) {
            const btn = document.createElement("button");
            btn.textContent = action.label;
            btn.className =
              "px-3 py-1.5 rounded-lg border border-gray-200 bg-white text-sm cursor-pointer hover:border-indigo-500 transition-colors";
            btn.addEventListener("click", () => {
              widget.submitMessage(action.message);
            });
            container.appendChild(btn);
          }

          target.appendChild(container);
        },
      },
    },
  });
  ```
</CodeGroup>

When the agent emits a `QuickActions` component with an array of label/message pairs, the user sees clickable buttons. Clicking one sends that message as if the user typed it.

## Example: clickable product cards

A more realistic example renders product or plan cards that let users drill into details with a single click.

<CodeGroup>
  ```tsx title="React"
  import { useRef } from 'react'
  import { InkeepEmbeddedChat } from '@inkeep/agents-ui-cloud'
  import type {
    AIChatFunctions,
    InkeepAIChatSettings,
    ComponentsConfig,
  } from '@inkeep/agents-ui-cloud/types'

  interface Plan {
    name: string
    price: string
    features: string[]
  }

  export default function PricingChat() {
    const chatFunctionsRef = useRef<AIChatFunctions | null>(null)

    const components: ComponentsConfig<{
      PlanCard: Plan
    }> = {
      PlanCard: (props) => (
        <div
          className="border border-gray-200 rounded-xl p-4 min-w-[200px] cursor-pointer hover:border-indigo-500 transition-colors"
          onClick={() =>
            chatFunctionsRef.current?.submitMessage(
              'Tell me more about the ' + props.name + ' plan. What is included and are there any limits?'
            )
          }
        >
          <div className="font-semibold text-base">{props.name}</div>
          <div className="text-xl font-bold my-2">{props.price}</div>
          <ul className="pl-4 m-0 text-sm text-gray-600">
            {props.features.map((f) => (
              <li key={f}>{f}</li>
            ))}
          </ul>
        </div>
      ),
    }

    const aiChatSettings: InkeepAIChatSettings = {
      appId: process.env.REACT_APP_INKEEP_APP_ID,
      chatFunctionsRef,
      components,
    }

    return <InkeepEmbeddedChat aiChatSettings={aiChatSettings} />
  }
  ```

  ```js title="JavaScript"
  const widget = Inkeep.EmbeddedChat("#ikp-embedded-chat-target", {
    aiChatSettings: {
      appId: "YOUR_APP_ID",
      components: {
        PlanCard: (props, target) => {
          const card = document.createElement("div");
          card.className =
            "border border-gray-200 rounded-xl p-4 min-w-[200px] cursor-pointer hover:border-indigo-500 transition-colors";

          const title = document.createElement("div");
          title.textContent = props.name;
          title.className = "font-semibold text-base";

          const price = document.createElement("div");
          price.textContent = props.price;
          price.className = "text-xl font-bold my-2";

          const list = document.createElement("ul");
          list.className = "pl-4 m-0 text-sm text-gray-600";
          for (const feature of props.features) {
            const li = document.createElement("li");
            li.textContent = feature;
            list.appendChild(li);
          }

          card.append(title, price, list);
          card.addEventListener("click", () => {
            widget.submitMessage(
              "Tell me more about the " + props.name + " plan. What is included and are there any limits?"
            );
          });

          target.appendChild(card);
        },
      },
    },
  });
  ```
</CodeGroup>

When a user asks "What plans do you offer?", the agent calls its tool to emit `PlanCard` components. The user sees styled cards and can click any card to ask a targeted follow-up, creating a natural conversational flow.

## Pre-filling the input

Instead of sending immediately, you can use `updateInputMessage` to pre-fill the input and let the user edit before sending. See the full list of available methods in the [AIChatFunctions reference](#aichatfunctions-reference) below.

<CodeGroup>
  ```tsx title="React"
  PlanCard: (props) => (
    <div
      className="border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-indigo-500 transition-colors"
      onClick={() => {
        chatFunctionsRef.current?.updateInputMessage(
          'I would like to learn more about ' + props.name
        )
        chatFunctionsRef.current?.focusInput()
      }}
    >
      <div className="font-semibold">{props.name}</div>
      <div className="text-xl font-bold my-2">{props.price}</div>
    </div>
  ),
  ```

  ```js title="JavaScript"
  PlanCard: (props, target) => {
    const card = document.createElement("div");
    card.className =
      "border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-indigo-500 transition-colors";

    const title = document.createElement("div");
    title.textContent = props.name;
    title.className = "font-semibold";

    const price = document.createElement("div");
    price.textContent = props.price;
    price.className = "text-xl font-bold my-2";

    card.append(title, price);
    card.addEventListener("click", () => {
      widget.updateInputMessage("I would like to learn more about " + props.name);
      widget.focusInput();
    });

    target.appendChild(card);
  },
  ```
</CodeGroup>

## Agent-side setup

For the agent to render these components, create **data components** in the Visual Builder. Each data component needs a name (matching the key in your `components` config) and a JSON schema defining its props.

<Steps>
  <Step>
    ### Create the data components

    Go to the **Data Components** tab in the left sidebar, then select **New data component**.

    For the `QuickActions` component, use the following schema:

    ```json title="QuickActions schema"
    {
      "type": "object",
      "properties": {
        "actions": {
          "type": "array",
          "description": "List of quick action buttons to display",
          "items": {
            "type": "object",
            "description": "A single quick action button",
            "properties": {
              "label": {
                "type": "string",
                "description": "Button label shown to the user"
              },
              "message": {
                "type": "string",
                "description": "Message to submit when clicked"
              }
            },
            "required": ["label", "message"]
          }
        }
      },
      "required": ["actions"]
    }
    ```

    For the `PlanCard` component:

    ```json title="PlanCard schema"
    {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Plan name, e.g. Starter, Pro, Enterprise"
        },
        "price": {
          "type": "string",
          "description": "Formatted price, e.g. $29/mo"
        },
        "features": {
          "type": "array",
          "description": "List of plan features",
          "items": {
            "type": "string",
            "description": "A single plan feature, e.g. Unlimited projects"
          }
        }
      },
      "required": ["name", "price", "features"]
    }
    ```
  </Step>

  <Step>
    ### Add the data components to your sub agent

    Open your agent's **Sub Agent** settings and add both data components. The agent will automatically use them when the conversation calls for it.

    See [Sub Agents](/visual-builder/sub-agents#adding-data-components) for details on adding data components.
  </Step>

  <Step>
    ### Register the components in your chat widget

    Use the same component names as keys in your `components` config, as shown in the [setup](#setup) section above. The names must match exactly.
  </Step>
</Steps>

For more on data components, see [Custom UI Blocks](/visual-builder/structured-outputs/data-components).

## Custom tool approval UI

When an agent tool requires human approval before executing, the widget shows a default approval card. You can replace this with your own UI by registering an `IkpTool` component.

`IkpTool` receives three props:

* `tool` - the tool invocation state (`ToolUIPart`). `tool.type` is the tool name as provided by the widget, prefixed with `tool-` (e.g. `tool-get_weather`). Also includes `tool.state`, `tool.input`, `tool.output`, and `tool.approval`.
* `approve` - a function to approve or deny the tool call. Call `approve(true)` to approve, `approve(false)` to deny.
* `renderMarkdown` - a helper to render markdown strings. In React, returns a React element. In JavaScript, returns a React node (use plain text rendering instead for vanilla JS).

### Tool states

| State                | Meaning                                  |
| -------------------- | ---------------------------------------- |
| `input-streaming`    | Tool input is being streamed             |
| `input-available`    | Tool input is ready, execution pending   |
| `approval-requested` | Waiting for user to approve or deny      |
| `approval-responded` | User has responded, execution proceeding |
| `output-available`   | Tool has completed successfully          |
| `output-error`       | Tool execution failed                    |
| `output-denied`      | User denied the tool call                |

### Example

<CodeGroup>
  ```tsx title="React"
  import { useRef } from 'react'
  import { InkeepEmbeddedChat } from '@inkeep/agents-ui-cloud'
  import type {
    AIChatFunctions,
    InkeepAIChatSettings,
    ComponentsConfig,
  } from '@inkeep/agents-ui-cloud/types'

  export default function ChatWithToolApproval() {
    const chatFunctionsRef = useRef<AIChatFunctions | null>(null)

    const components: ComponentsConfig<{}> = {
      IkpTool: (props) => {
        const { tool, approve, renderMarkdown } = props
        const toolName = tool.type.replace(/^tool-/, '')
        const awaitingApproval = tool.state === 'approval-requested'

        return (
          <div className="border rounded-lg p-4 my-2">
            <div className="flex items-center gap-2 mb-2">
              <span className="font-medium text-sm">{toolName}</span>
              <span className="text-xs text-gray-500 capitalize">{tool.state}</span>
            </div>

            {tool.input != null && (
              <pre className="text-xs bg-gray-50 rounded p-2 mb-3 overflow-auto">
                {JSON.stringify(tool.input, null, 2)}
              </pre>
            )}

            {awaitingApproval && (
              <div className="flex gap-2 justify-end">
                <button
                  className="px-3 py-1 text-sm rounded border border-gray-300 hover:bg-gray-50"
                  onClick={() => approve(false)}
                >
                  Deny
                </button>
                <button
                  className="px-3 py-1 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-700"
                  onClick={() => approve(true)}
                >
                  Approve
                </button>
              </div>
            )}

            {tool.state === 'output-available' && tool.output != null && (
              <div className="text-sm text-green-700 mt-2">
                {renderMarkdown(String(tool.output))}
              </div>
            )}

            {tool.state === 'output-denied' && (
              <div className="text-sm text-red-600 mt-2">Tool call denied</div>
            )}

            {tool.state === 'output-error' && (
              <div className="text-sm text-red-600 mt-2">{tool.errorText}</div>
            )}
          </div>
        )
      },
    }

    const aiChatSettings: InkeepAIChatSettings = {
      appId: process.env.REACT_APP_INKEEP_APP_ID,
      chatFunctionsRef,
      components,
    }

    return <InkeepEmbeddedChat aiChatSettings={aiChatSettings} />
  }
  ```

  ```js title="JavaScript"
  const widget = Inkeep.EmbeddedChat("#ikp-embedded-chat-target", {
    aiChatSettings: {
      appId: "YOUR_APP_ID",
      components: {
        IkpTool: (props, target) => {
          const { tool, approve, renderMarkdown } = props;
          const toolName = tool.type.replace(/^tool-/, "");

          const card = document.createElement("div");
          card.className = "border rounded-lg p-4 my-2";

          // Header
          const header = document.createElement("div");
          header.className = "flex items-center gap-2 mb-2";

          const nameSpan = document.createElement("span");
          nameSpan.className = "font-medium text-sm";
          nameSpan.textContent = toolName;

          const stateSpan = document.createElement("span");
          stateSpan.className = "text-xs text-gray-500 capitalize";
          stateSpan.textContent = tool.state;

          header.append(nameSpan, stateSpan);
          card.appendChild(header);

          // Tool input
          if (tool.input != null) {
            const pre = document.createElement("pre");
            pre.className = "text-xs bg-gray-50 rounded p-2 mb-3 overflow-auto";
            pre.textContent = JSON.stringify(tool.input, null, 2);
            card.appendChild(pre);
          }

          // Approval buttons
          if (tool.state === "approval-requested") {
            const actions = document.createElement("div");
            actions.className = "flex gap-2 justify-end";

            const denyBtn = document.createElement("button");
            denyBtn.textContent = "Deny";
            denyBtn.className = "px-3 py-1 text-sm rounded border border-gray-300 hover:bg-gray-50";
            denyBtn.addEventListener("click", () => approve(false));

            const approveBtn = document.createElement("button");
            approveBtn.textContent = "Approve";
            approveBtn.className = "px-3 py-1 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-700";
            approveBtn.addEventListener("click", () => approve(true));

            actions.append(denyBtn, approveBtn);
            card.appendChild(actions);
          }

          // Output states
          if (tool.state === "output-available" && tool.output != null) {
            const msg = document.createElement("div");
            msg.className = "text-sm text-green-700 mt-2";
            msg.textContent = String(tool.output);
            card.appendChild(msg);
          }

          if (tool.state === "output-denied") {
            const msg = document.createElement("div");
            msg.className = "text-sm text-red-600 mt-2";
            msg.textContent = "Tool call denied";
            card.appendChild(msg);
          }

          if (tool.state === "output-error") {
            const msg = document.createElement("div");
            msg.className = "text-sm text-red-600 mt-2";
            msg.textContent = tool.errorText;
            card.appendChild(msg);
          }

          target.appendChild(card);
        },
      },
    },
  });
  ```
</CodeGroup>

`IkpTool` applies globally to all tool calls. To render different UI per tool, branch on `tool.type`:

```tsx title="per-tool-ui.tsx"
IkpTool: (props) => {
  const { tool, approve } = props
  const toolName = tool.type.replace(/^tool-/, '')

  if (toolName === 'database-query') {
    return <DatabaseQueryApproval tool={tool} approve={approve} />
  }

  if (toolName === 'send-email') {
    return <EmailApproval tool={tool} approve={approve} />
  }

  // Fall back to a generic card for other tools
  return <GenericToolCard tool={tool} approve={approve} />
},
```

## AIChatFunctions reference

<AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />


# Using the Slack App (/talk-to-your-agents/slack/commands)

How to talk to Inkeep agents in Slack — commands, mentions, and tips.



This guide is for **all team members** using the Inkeep Slack app. You'll learn how to ask questions, understand which agent responds, and get the most out of the integration.

## Before you start

You need to link your Slack account to Inkeep (one-time setup). The first time you interact with the bot — via `@Inkeep` or any `/inkeep` command — you'll automatically receive a **Link Account** button. Click it, sign in to Inkeep, and your original question will be answered automatically.

You can also link proactively by running `/inkeep link` in any channel.

Run `/inkeep status` to confirm your account is linked.

## Mentioning `@Inkeep`

Mention `@Inkeep` in any channel to ask a question. The agent responds in a thread.

| Usage                                    | What happens                                                                   |
| ---------------------------------------- | ------------------------------------------------------------------------------ |
| `@Inkeep <your question>`                | Posts a response in a thread, visible to the whole channel                     |
| `@Inkeep <question>` in a thread         | Includes the full thread as context for a more relevant answer                 |
| `@Inkeep` in a thread (no message)       | Uses the entire thread as the question — great for "explain this conversation" |
| `@Inkeep` with no message (in a channel) | Shows a usage hint                                                             |

<Note>
  The `@Inkeep` message itself is posted to the channel by Slack — this is standard Slack behavior and can't be suppressed.
</Note>

### Thread context

When you use `@Inkeep` inside a thread, the agent automatically receives the full conversation history as context. This is useful for:

* Getting a summary of a long discussion
* Asking follow-up questions about something in the thread
* Having the agent help draft a response

## Slash command — `/inkeep`

Use the `/inkeep` slash commands to talk to an agent or manage your Inkeep integration.

| Command                   | What it does                                                  |
| ------------------------- | ------------------------------------------------------------- |
| `/inkeep <your question>` | Send a question to the default agent                          |
| `/inkeep`                 | Open a modal to pick a project, agent, and write your prompt  |
| `/inkeep help`            | Show the full usage guide                                     |
| `/inkeep status`          | See your linked account and the active agent for this channel |
| `/inkeep link`            | Link your Slack account to Inkeep (one-time)                  |
| `/inkeep unlink`          | Disconnect your Slack account                                 |

### Agent picker modal

Running `/inkeep` with no arguments opens a modal where you can:

1. Select a **project** from the dropdown
2. Select an **agent** from that project (the list updates as you change projects)
3. Optionally write a **prompt** (or submit without a message to invoke the agent directly)
4. Submit to send your question

<Tip>
  To continue a conversation, `@Inkeep` the bot in the response thread — the agent receives the full thread as context.
</Tip>

## Message shortcuts

You can send any message to an agent using Slack's message shortcuts:

* **Desktop** — right-click a message, or hover over it and click the **⋮** (more actions) icon, then select **Connect to Apps** → **Talk to an Inkeep Agent**
* **Mobile** — long-press a message, tap the **⋮** (more actions) icon, then select **Connect to Apps** → **Talk to an Inkeep Agent**

This opens a modal where you pick a project and agent, add any additional instructions (optional), then submit. The agent's response appears as a **threaded reply on the target message**, with the original message text as context.

## Direct messages

You can mention `@Inkeep` or use `/inkeep` commands in your DMs for private conversations.

| Usage                             | What happens                                     |
| --------------------------------- | ------------------------------------------------ |
| `@Inkeep <your question>` in a DM | The workspace default agent responds in a thread |
| `/inkeep <your question>` in a DM | The workspace default agent responds             |
| Reply in a DM thread              | The agent receives the full DM thread as context |

DMs use your **workspace default agent**. Channel-specific agent overrides do not apply in DMs. You must have your account linked and have access to the agent's project.

## Which agent responds?

The agent that handles your request depends on how your admin configured the workspace:

| Priority | What's checked                                                            | Example                      |
| -------- | ------------------------------------------------------------------------- | ---------------------------- |
| **1st**  | Channel default — has an admin assigned a specific agent to this channel? | `#support` → "Support Agent" |
| **2nd**  | Workspace default — the fallback agent for all channels                   | "General Assistant"          |

**In practice:**

* If you're in `#support` and the admin set a "Support Agent" for that channel → you get the Support Agent
* If you're in `#random` with no channel-specific agent → you get the workspace default
* If no agent is configured at all → the bot tells you to ask an admin to set one up

<Tip>
  Run `/inkeep status` to see exactly which agent is active in the current channel and whether it's a channel default or workspace default.
</Tip>

## Tool approvals

Some agents use tools that require your approval before they run. When this happens, you'll see an interactive message in the thread with the tool name, its inputs, and **Approve** / **Deny** buttons.

### Approving or denying

* Click **Approve** to let the agent run the tool
* Click **Deny** to block the tool — the agent will acknowledge and may suggest alternatives

Only the person who started the conversation can approve or deny. If you're not the requester, you'll see an error if you try to click the buttons.

### Approval timeout

If you don't respond within a few minutes, the approval request expires. The message updates to show "Expired" and the agent is notified.

<Tip>
  If you frequently use agents that require tool approvals, make sure your account is linked (`/inkeep link`) beforehand to avoid interruptions.
</Tip>

## Troubleshooting

### "You need to link your account"

Click the **Link Account** button in the prompt to connect your Slack and Inkeep accounts. This is a one-time setup per user. If you don't see the button, run `/inkeep link` to get a new link.

### "No agents configured"

Ask your workspace admin to set a workspace or channel default agent in the Inkeep dashboard at **Work Apps** → **Slack**.

### "Request timed out"

The agent took too long to respond. Try again — if the issue persists, ask your admin to check that the Inkeep deployment is running and healthy.

### Command shows "failed with error"

Check that:

* The Slack app is properly installed (try `/inkeep help`)
* Your account is linked (`/inkeep status`)
* An agent is configured for the workspace


# Configure Slack Agents (/talk-to-your-agents/slack/configuration)

Admin guide — set up workspace and channel default agents for the Slack integration.



<Note>
  This page is for **workspace administrators**. Regular team members should see [Using the Slack App](/talk-to-your-agents/slack/commands) for usage instructions.
</Note>

After installing the Slack app, configure which agents respond to your team's messages. You can set a workspace-wide default and optionally assign different agents to specific channels.

## Workspace default

The workspace default agent responds to all `@Inkeep` mentions and `/inkeep` commands across every channel — unless a channel has its own default.

<Steps>
  <Step>
    ### Open the Slack dashboard

    Navigate to **Work Apps** → **Slack** in the Inkeep dashboard. Select your installed workspace.
  </Step>

  <Step>
    ### Set the default agent

    In the **Agent Configuration** section, select a project and agent from the dropdowns. Click **Save**.

    This agent handles all requests in channels without a channel-specific default.
  </Step>

  <Step>
    ### Remove the default agent (optional)

    To clear the workspace default, open the dropdown and select **Remove default agent**. Channels without a channel-specific agent will no longer have a fallback.
  </Step>
</Steps>

## Channel defaults

Channel defaults let you assign a specific agent to individual channels. This is useful when different channels need different expertise — for example, a support agent for `#support` and a technical agent for `#engineering`.

<Steps>
  <Step>
    ### Open channel defaults

    In the Slack dashboard, scroll to the **Channel Defaults** section. You'll see a list of channels where the Inkeep bot is a member.

    <Note>
      Channels only appear in this list if the Inkeep bot is a member. To add a channel, open it in Slack, add `@Inkeep` as a member, and the channel will then be available for configuration. If the list is empty, the bot hasn't been added to any channels yet.
    </Note>
  </Step>

  <Step>
    ### Filter and select channels

    Use the filter buttons to view **All**, **Private**, or **Slack Connect** channels.
  </Step>

  <Step>
    ### Assign an agent

    Choose a project and agent for the selected channels. You can configure channels individually or use **bulk actions** to assign the same agent to multiple channels at once.
  </Step>
</Steps>

### Grant access to channel members

When you assign an agent to a channel, a **Grant access to members** toggle appears in the channel's agent selector. This controls whether channel members can use the assigned agent without being explicit project members.

| Setting               | Behavior                                                                                                                                                                           |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Enabled** (default) | Channel members can use the agent via `@Inkeep` and `/inkeep` without needing explicit project access. The channel configuration acts as implicit authorization.                   |
| **Disabled**          | Channel members must have explicit project access (configured separately) to use the agent. The channel default still determines *which* agent responds, but doesn't grant access. |

<Tip>
  This toggle defaults to **enabled** — assigning an agent to a channel automatically grants access to all channel members. Disable it if you want to control project access separately (e.g., for channels where only certain members should use the agent).
</Tip>

<Note>
  The grant access setting applies per channel. Workspace defaults grant access by default — the toggle is only available in the UI for channel-level overrides.
</Note>

## Agent resolution priority

When a team member uses `@Inkeep` or `/inkeep`, the app resolves which agent to use:

| Priority | Source                | Scope                 | Example                               |
| -------- | --------------------- | --------------------- | ------------------------------------- |
| 1        | **Channel default**   | Specific channel only | `#support` → "Support Agent"          |
| 2        | **Workspace default** | All other channels    | Everything else → "General Assistant" |

<Tip>
  Channel defaults take priority over the workspace default. If you remove a channel default, that channel falls back to the workspace default agent.
</Tip>

## Managing linked users

The dashboard shows all users who have linked their Slack accounts to Inkeep. You can:

* **View** linked users with their Slack username, email, and link date
* **Export** the linked users list as CSV
* **Unlink** a user if needed (the user can re-link with `/inkeep link`)

## Workspace health

The dashboard includes a health check that verifies:

* **Bot connection** — The bot token is valid and the bot is accessible
* **Permissions** — The bot can post messages, read channels, and read history
* **Workspace info** — Team name and bot identity are correct

If the health check shows issues, try reinstalling the Slack app from the dashboard.

## Permission model

| Action                      | Required role                              |
| --------------------------- | ------------------------------------------ |
| Install / uninstall the app | Organization admin or owner                |
| Set workspace default agent | Organization admin or owner                |
| Set channel defaults        | Organization admin, or channel member      |
| View configuration          | Any authenticated user in the organization |

## Next steps

<Cards>
  <Card title="Using the Slack App" icon="LuMessageSquare" href="/talk-to-your-agents/slack/commands">
    Share this with your team — command reference and usage guide
  </Card>

  <Card title="API Reference" icon="LuFileCode" href="/api-reference/slack">
    Explore the Slack API endpoints
  </Card>
</Cards>


# Install and Set Up the Slack App (/talk-to-your-agents/slack/installation)

Admin guide — install the Inkeep Slack app, configure agents, and onboard your team.



This guide is for **workspace administrators**. It walks you through installing the Inkeep Slack app, configuring which agents respond, and getting your team onboarded.

<Note>
  The Slack integration is only available for [Inkeep Enterprise](https://inkeep.com/enterprise?cta_id=docs_nav).
</Note>

## Prerequisites

* An Inkeep Enterprise account with at least one project and agent configured
* **Slack workspace admin** access (required to install apps)

## Install the app

<Steps>
  <Step>
    ### Open the Work Apps dashboard

    Navigate to your Inkeep dashboard and select **Work Apps** from the sidebar. You'll see the Slack integration card.
  </Step>

  <Step>
    ### Install to Slack

    Click **Install** on the Slack card. This redirects you to Slack's OAuth consent screen where you authorize the Inkeep bot to join your workspace.

    The app requests 19 Slack scopes for reading messages, posting replies, and managing conversations.

    <Accordions>
      <Accordion title="View the full Slack permissions list">
        | Scope               | Token type   | Reason                                                                                                                                             |
        | ------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
        | `app_mentions:read` | Bot          | Receive `app_mention` events when users mention Inkeep in channels. This is the primary way users invoke the agent in public and private channels. |
        | `channels:history`  | Bot          | Retrieve public-channel thread context after a user mentions Inkeep, so the bot can provide contextually relevant answers.                         |
        | `channels:read`     | Bot          | List and inspect public channel metadata, members, and available conversations for configuration and bot membership checks.                        |
        | `chat:write`        | Bot          | Post thread responses, ephemeral status messages, and streaming-response updates as Inkeep.                                                        |
        | `chat:write.public` | Bot          | Post messages to public channels where Inkeep has not been explicitly added, including test messages and diagnostics.                              |
        | `commands`          | Bot          | Register and handle the `/inkeep` slash command for asking questions, linking accounts, checking status, and getting help.                         |
        | `files:read`        | Bot          | Read files shared with Inkeep so agents can parse PDFs, images, and other attachments sent by users.                                               |
        | `files:write`       | Bot          | Upload large responses or structured data as Slack file attachments when content exceeds Slack message limits.                                     |
        | `groups:history`    | Bot          | Retrieve private-channel thread context when users mention Inkeep in private channels.                                                             |
        | `groups:read`       | Bot          | List private channels for configuration and validate bot membership in private channels.                                                           |
        | `im:history`        | Bot          | Retrieve direct-message thread context when users continue a conversation with Inkeep in DMs.                                                      |
        | `im:read`           | Bot          | Look up direct-message channel metadata.                                                                                                           |
        | `im:write`          | Bot          | Start direct messages with users, such as account-linking prompts and approval requests.                                                           |
        | `mpim:history`      | Bot          | Retrieve group-DM thread context and prior messages for relevant multi-user responses.                                                             |
        | `mpim:read`         | Bot          | Look up group-DM metadata, including participants and channel name.                                                                                |
        | `mpim:write`        | Bot          | Start group direct messages, such as shared escalation conversations with multiple users.                                                          |
        | `team:read`         | Bot          | Retrieve workspace metadata, such as name, domain, and icon URL, for display in the Inkeep dashboard and workspace identification.                 |
        | `users:read`        | User and bot | Look up Slack user profiles, resolve user identities for account linking, and list workspace members for MCP user lookup.                          |
        | `users:read.email`  | User and bot | Match Slack users to Inkeep accounts by email during account linking and enrich user context for agent interactions.                               |
      </Accordion>
    </Accordions>
  </Step>

  <Step>
    ### Verify the installation

    After authorizing, you're redirected back to the dashboard. The Slack card now shows your workspace as **Installed** with the workspace name.

    Verify the bot is active by typing `/inkeep help` in any Slack channel.
  </Step>
</Steps>

<Note>
  Only one Slack workspace can be connected to your Inkeep tenant. To switch workspaces, uninstall the existing one first.
</Note>

## Configure agents

After installing, set up which agent responds to your team's requests. See the full [Configuration guide](/talk-to-your-agents/slack/configuration) for details.

1. Go to **Work Apps** → **Slack** in the dashboard
2. Set default agents for your workspace and channels

<Tip>
  Channel defaults take priority over the workspace default. If you remove a channel default, that channel falls back to the workspace default agent.
</Tip>

## Onboard your team

Account linking happens automatically — when an unlinked user first interacts with the bot (via `@Inkeep` or `/inkeep`), they receive a prompt with a **Link Account** button. After clicking it and signing in to Inkeep, their original question is automatically answered. This only needs to happen once per user.

Alternatively, users can link proactively by running `/inkeep link` in any channel.

<Tip>
  The link button uses a short-lived JWT token (10 minutes) for security. If it expires, the user can simply ask their question again to get a new prompt, or run `/inkeep link` manually.
</Tip>

<Cards>
  <Card title="Configuration" icon="LuSettings" href="/talk-to-your-agents/slack/configuration">
    Set up workspace defaults, channel defaults, and manage users
  </Card>

  <Card title="Using the Slack App" icon="LuMessageSquare" href="/talk-to-your-agents/slack/commands">
    Share this with your team — full command and usage reference
  </Card>
</Cards>

## Uninstalling

To remove the Slack app from your workspace:

1. Go to **Work Apps** → **Slack** in the dashboard
2. Click the workspace options menu
3. Select **Uninstall**

This removes the bot from your workspace, deletes all user link mappings, and cleans up channel configurations.


# Slack MCP Tool (/talk-to-your-agents/slack/mcp-tool)

Give your agents the ability to post messages to Slack channels and DMs via the MCP protocol.



The Slack MCP tool lets your agents **post messages to Slack** — channels, threads, and DMs. Unlike the [Slack Work App](/talk-to-your-agents/slack/overview) (where users talk *to* agents in Slack), the MCP tool gives agents the ability to act *on* Slack as part of their workflows.

<Note>
  The Slack MCP tool requires an installed Slack workspace. Follow the [installation guide](/talk-to-your-agents/slack/installation) first if you haven't set up the Slack app yet.
</Note>

## Create a Slack MCP tool

<Steps>
  <Step>
    ### Open MCP Servers

    Navigate to **MCP Servers** in the sidebar and click **New MCP server**.
  </Step>

  <Step>
    ### Select Slack from Work Apps

    Switch to the **Work Apps** tab and select the **Slack** card. If your organization has a Slack workspace installed, you'll see the channel configuration dialog.

    If no workspace is installed, the dialog prompts you to [install the Slack app](/talk-to-your-agents/slack/installation) first.
  </Step>

  <Step>
    ### Configure channel access

    Choose how the agent can interact with Slack:

    | Setting                 | Description                                                                                                                                      |
    | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
    | **Channel access mode** | **All channels** — the agent can post to any channel where the bot is a member. **Selected channels** — restrict the agent to specific channels. |
    | **DM enabled**          | Whether the agent can send direct messages. Enabled by default.                                                                                  |
    | **Selected channels**   | When using "Selected channels" mode, pick from the list of channels where the Inkeep bot is a member.                                            |

    Channel access and DM access are independent — you can allow all channels but disable DMs, or allow selected channels and also enable DMs.
  </Step>

  <Step>
    ### Create

    Click **Create** to create the Slack MCP tool. It appears in your MCP servers list and can be added to any agent.
  </Step>
</Steps>

## Available tools

The Slack MCP server exposes the following tools:

### `post-channel-message`

Posts a message to a Slack channel.

### `post-direct-message`

Sends a direct message to a Slack user.

The agent knows how to send a DM to the user asking the question out of the box — no additional prompting or configuration is required.

### `get-slack-user`

Looks up a Slack user by their user ID, email address, or name. Provide exactly one parameter:

| Parameter | Description                                                                         |
| --------- | ----------------------------------------------------------------------------------- |
| `user_id` | Slack user ID (e.g., `U1234567890`). Returns an exact match.                        |
| `email`   | Email address (e.g., `jane@company.com`). Returns an exact match.                   |
| `name`    | Display name, real name, or username to search for. Returns up to 5 ranked matches. |

When searching by name, the agent should clarify with the user if multiple results are returned.

## Edit channel access

To change channel access after creation:

1. Open the Slack MCP tool from the **MCP Servers** list
2. In the **Slack Channel Access** section, click **Configure**
3. Update the channel access mode, DM toggle, or selected channels
4. Click **Save Changes**

## Access control

The MCP tool enforces access rules at runtime:

* **Selected channels mode**: The agent can only post to channels in the allowed list. Attempts to post to other channels are rejected.
* **DM disabled**: The agent cannot post to DM channels (channel IDs starting with `D`). Enable the DM toggle to allow DMs.
* **All channels mode**: The agent can post to any channel where the bot is a member.

Access configuration is per-tool — different agents can have different Slack MCP tools with different channel restrictions.


# Slack Work App (/talk-to-your-agents/slack/overview)

Connect your Inkeep agents to Slack.



The Slack Work App brings your Inkeep agents directly into Slack. Team members can talk to agents by mentioning `@Inkeep` or by using `/inkeep` commands.

<Note>
  The Slack integration is only available for [Inkeep Enterprise](https://inkeep.com/enterprise?cta_id=docs_nav).
</Note>

## Key features

* **`@Inkeep` and `/inkeep`** — Mention `@Inkeep` or use `/inkeep` commands in a channel or your DMs to talk to an Inkeep agent.
* **Channel configuration** — Set a workspace-wide default agent, or assign specific agents to individual channels

## Which agent handles my request?

The app uses an **agent resolution priority** to decide which agent responds:

| Priority | Source                | Set by                   | Scope                                  |
| -------- | --------------------- | ------------------------ | -------------------------------------- |
| 1        | **Channel default**   | Admin, via the dashboard | Only that channel                      |
| 2        | **Workspace default** | Admin, via the dashboard | All channels without a channel default |

If no agent is configured at either level, the bot prompts you to ask an admin to set one up in the dashboard.

<Tip>
  Use `/inkeep status` in any channel to see which agent is active for that channel and how it was resolved.
</Tip>

## Get started

<Cards>
  <Card title="Admin: Install & Configure" icon="LuShield" href="/talk-to-your-agents/slack/installation">
    Guide for Slack workspace admins — install the app, set up agents, manage users
  </Card>

  <Card title="Using the Slack App" icon="LuMessageSquare" href="/talk-to-your-agents/slack/commands">
    Guide for all team members — how to ask questions, link your account, and use commands
  </Card>

  <Card title="Dashboard Configuration" icon="LuSettings" href="/talk-to-your-agents/slack/configuration">
    Guide for admins — workspace defaults, channel defaults, and user management
  </Card>

  <Card title="Slack MCP Tool" icon="LuServer" href="/talk-to-your-agents/slack/mcp-tool">
    Give agents the ability to post messages to Slack channels and DMs
  </Card>
</Cards>


# Build chat UIs with Vercel AI Elements (/talk-to-your-agents/vercel-ai-sdk/ai-elements)

Use Vercel AI Elements to quickly assemble production-ready chat interfaces that connect to your Inkeep agent.



## Overview

[Vercel AI Elements](https://ai-sdk.dev/elements/overview) is a set of prebuilt, headless-first UI primitives for building AI chat interfaces. Think of it as a `shadcn/ui`-like library of UI components for conversational experiences.

The library works out of the box with Inkeep Agents via the `/run/api/chat` [API endpoint](/talk-to-your-agents/chat-api) since it follows the streaming protocol used by AI Elements.

For fully custom `useChat` hooks and lower-level control, see the [useChat guide](/talk-to-your-agents/vercel-ai-sdk/use-chat).

## Using AI Elements

```tsx
import { Conversation, Message, PromptInput, Actions } from '@ai-sdk/react-elements';
import { useChat } from '@ai-sdk/react';

export default function ChatWithElements() {
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: "${process.env.NEXT_PUBLIC_INKEEP_AGENTS_API_URL}/run/api/chat",
      headers: {
        "x-inkeep-tenant-id": "default",
        "x-inkeep-project-id": "activities-planner",
        "x-inkeep-agent-id": "activities-planner",
      },
    })
  });

  return (
    <Conversation messages={messages}>
      {messages.map(message => (
        <Message key={message.id} message={message}>
          <Actions message={message} />
        </Message>
      ))}
      <PromptInput onSubmit={sendMessage} />
    </Conversation>
  );
}
```

## Install (one-liner)

```sh
npx ai-elements@latest
```

This CLI adds selected components to your project under `@/components/ai-elements/` and wires up any required shadcn/ui primitives.

## Component quick reference

* **Core chat**: `Conversation`, `Message`, `PromptInput`, `Response`, `Actions`, `Sources`
* **Reasoning & tools**: `Reasoning`, `Tool`, `Task`, `InlineCitation`, `Context`
* **Workflow (React Flow)**: `Canvas`, `Node`, `Edge`, `Connection`, `Controls`, `Panel`, `Toolbar`
* **Coding artifacts**: `CodeBlock`, `Artifact`, `WebPreview`, `Image`
* **UX extras**: `Suggestion`, `Branch`, `Loader`, `OpenIn`

See the [AI Elements](https://ai-sdk.dev/elements/overview) site for detailed props and examples.


# Use Inkeep agents with Vercel AI SDK (/talk-to-your-agents/vercel-ai-sdk/inkeep-provider)

A custom AI SDK provider that lets you use Inkeep agents with generateText, streamText, and other AI SDK functions.



## Overview

The `@inkeep/ai-sdk-provider` package is a custom AI SDK provider that exposes Inkeep agents through the Vercel AI SDK. This lets you call your agents with `generateText()` and `streamText()`.

## Installation

```bash
npm install @inkeep/ai-sdk-provider ai
```

## Basic Usage

<Tip>
  If connecting to Inkeep Enterprise, use `https://api.agents.inkeep.com/run/api/chat` as your `INKEEP_AGENTS_API_URL`.
</Tip>

### Text Generation

```typescript
import { generateText } from 'ai';
import { createInkeep } from '@inkeep/ai-sdk-provider';

const inkeep = createInkeep({
  baseURL: process.env.INKEEP_AGENTS_API_URL, // Required, for local development this is typically http://localhost:3002
  apiKey: '<your-agent-api-key>', // Created in the Agents Dashboard
  // headers: { // Optional if you are developing locally and don't want to use an api key
  //   'x-inkeep-agent-id': 'your-agent-id',
  //   'x-inkeep-tenant-id': 'your-tenant-id',
  //   'x-inkeep-project-id': 'your-project-id',
  // },
});

const { text } = await generateText({
  model: inkeep(),
  prompt: 'What is the weather in NYC?',
});

console.log(text);
```

### Streaming Responses

```typescript
import { streamText } from 'ai';
import { createInkeep } from '@inkeep/ai-sdk-provider';

const inkeep = createInkeep({
  baseURL: process.env.INKEEP_AGENTS_API_URL,
  apiKey: '<your-agent-api-key>', // The agent ID is encoded in the API key
  headers: {
    'x-emit-operations': 'true', // Enable tool event streaming
  },
});

const result = await streamText({
  model: inkeep(),
  prompt: 'Plan an event in NYC',
});

for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}
```

## Configuration

### Provider Settings

```typescript
createInkeep({
  baseURL: string,        // Required. Your agents-run-api URL
  apiKey?: string,        // Optional. Bearer token for authentication
  headers?: Record<string, string>, // Optional. Additional headers
})
```

### Provider Options

Pass options when creating a provider instance:

```typescript
const provider = inkeep({
  conversationId: 'conv-456',
  headers: { 'user-id': 'user-789' },
});
```

## Tool Call Observability

To receive tool call and result events in your stream, include the `x-emit-operations` header:

```typescript
const inkeep = createInkeep({
  baseURL: process.env.INKEEP_AGENTS_API_URL,
  apiKey: '<your-agent-api-key>', // The agent ID is encoded in the API key
  headers: {
    'x-emit-operations': 'true',
  },
});

const result = await streamText({
  model: inkeep(),
  prompt: 'Search for recent papers on AI',
});

for await (const event of result.fullStream) {
  switch (event.type) {
    case 'tool-call':
      console.log(`Calling: ${event.toolName}`);
      break;
    case 'tool-result':
      console.log(`Result: ${event.toolName}`);
      break;
  }
}
```

<Note>
  Tool approvals are interactive: when a tool requires approval, you must approve/deny the pending tool call for the run to continue.
  For end-user UIs, prefer the [Chat API](/talk-to-your-agents/chat-api) and implement [Tool approvals](/typescript-sdk/tools/tool-approvals).
</Note>

### Supported Stream Events

**Text events** (always emitted):

* `text-start`, `text-delta`, `text-end`

**Tool events** (requires `x-emit-operations: true`):

* `tool-call` - Tool invocations (including agent transfers and delegations)
* `tool-result` - Tool results (including delegation returns)

**Control events** (always emitted):

* `finish` - Stream completion with usage stats
* `error` - Stream errors

## Next Steps

<Cards>
  <Card title="useChat Hook" icon="LuCode" href="/talk-to-your-agents/vercel-ai-sdk/use-chat">
    Build React chat interfaces with streaming responses.
  </Card>

  <Card title="AI Elements" icon="LuPackage" href="/talk-to-your-agents/vercel-ai-sdk/ai-elements">
    Use prebuilt UI components for chat experiences.
  </Card>

  <Card title="Chat API" icon="LuNetwork" href="/talk-to-your-agents/chat-api">
    Learn about the underlying HTTP API endpoint.
  </Card>
</Cards>


# Create custom UIs using the Vercel AI SDK (/talk-to-your-agents/vercel-ai-sdk/use-chat)

Use the Vercel AI SDK's useChat hook to build React chat interfaces that stream responses from your agent.



## Overview

Agents built with Inkeep have an `/api/chat` [API endpoint](/talk-to-your-agents/chat-api) that is compatible with Vercel AI SDK's streaming protocol, so it works out of the box with Vercel's AI SDK ecosystem.

To build totally custom UIs, you have two options in the AI SDK ecosystem:

* Use `useChat` hook to build custom UIs in React, Svelte, Vue.js, and Angular. (**← this guide**)
* Use the [AI Elements](/talk-to-your-agents/vercel-ai-sdk/ai-elements) library for a shadcn-like chat primitives.

<Tip>
  If your agent uses tools that require user confirmation, the stream can include tool approval requests. See [Tool approvals](/typescript-sdk/tools/tool-approvals).
</Tip>

## Using `useChat`

Install the Vercel AI SDK in your React application:

```bash
npm install ai @ai-sdk/react
```

## Basic Configuration

Here's a minimal example of using `useChat` with your agent:

```tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";

export default function Page() {
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: "${process.env.NEXT_PUBLIC_INKEEP_AGENTS_API_URL}/run/api/chat", // 
      headers: {
        "x-inkeep-tenant-id": "default",
        "x-inkeep-project-id": "activities-planner",
        "x-inkeep-agent-id": "activities-planner",
      },
    }),
  });
  const [input, setInput] = useState("");

  return (
    <div className="max-w-2xl mx-auto p-4">
      <div className="space-y-4 mb-4">
        {messages.map((message) => (
          <div key={message.id} className="border rounded p-3">
            <div className="font-semibold mb-2">
              {message.role === "user" ? "👤 User" : "🤖 Assistant"}
            </div>

            <div className="space-y-2">
              {message.parts.map((part, partIndex) => {
                const partKey = `${message.id}-${part.type}-${partIndex}`;

                if (part.type === "text") {
                  return (
                    <div key={partKey} className="whitespace-pre-wrap">
                      {part.text}
                    </div>
                  );
                }
                
                return null;
              })}
            </div>
          </div>
        ))}
      </div>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (input.trim()) {
            sendMessage({ text: input });
            setInput("");
          }
        }}
        className="flex gap-2"
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Ask about weather, request confirmation, or ask for location..."
          className="flex-1 p-2 border rounded"
        />
        <button
          type="submit"
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          Send
        </button>
      </form>

      <div className="mt-4 text-sm text-gray-600">
        <p>Try asking:</p>
        <ul className="list-disc list-inside space-y-1">
          <li>"What's the weather in New York?"</li>
          <li>"Can you get my location?"</li>
          <li>"Ask me to confirm something"</li>
        </ul>
      </div>
    </div>
  );
}
```

## Configuration Options

### API Endpoint

The `api` parameter should point to your Run API's chat endpoint:

* **Local development**: `http://localhost:3002/run/api/chat`
* **Production**: `https://your-run-api-domain.com/run/api/chat`

<>
  ## Authentication

  Choose the authentication method:

  <Tabs>
    <Tab title="App Credential (app-based)">
      App credentials are the auth method for app-based integrations such as web clients, support copilots, and authenticated end-user experiences. For web clients, the App ID is safe to expose client-side.

      **Setup:**

      1. [Create an app credential](/talk-to-your-agents/app-credentials#create-an-app-via-api) for your project
      2. Request an anonymous session token from your client
      3. Use the token and App ID in chat requests

      **Request Headers:**

      ```http
      Authorization: Bearer <session_token>
      X-Inkeep-App-Id: <app_id>
      ```

      See [App Credentials](/talk-to-your-agents/app-credentials) for the full auth flow details.
    </Tab>

    <Tab title="API Key (server-to-server)">
      Use API keys for server-to-server integrations where the secret is never exposed to end-users.

      **Create an API Key:**

      1. Open the Visual Builder Dashboard
      2. Go to your Project → **API Keys**
      3. Click **Create**, select the target agent
      4. Copy the API key (it will be shown once) and store it securely

      **Request Header:**

      ```http
      Authorization: Bearer <api_key>
      ```

      <Warning>
        API keys are secrets. Never expose them in client-side code or browser requests.
      </Warning>
    </Tab>

    <Tab title="Development (no auth)">
      When running the API server locally with `pnpm dev`, authentication is automatically bypassed. You can use headers in the request instead:

      **Request Headers:**

      ```http
      x-inkeep-tenant-id: <tenant-id>
      x-inkeep-project-id: <project-id>
      x-inkeep-agent-id: <agent-id>
      ```

      <Warning>
        This mode is for development only. Never use in production as it bypasses all security checks.
      </Warning>
    </Tab>
  </Tabs>

  See [Authentication → Run API](/api-reference/authentication/run-api) for more details.
</>

### Optional Configuration

```tsx
const { messages, sendMessage, status } = useChat({
  api: 'http://localhost:3002/run/api/chat',
  headers: { /* ... */ },

  // Handle completion
  onFinish: (message) => {
    console.log('Agent finished responding:', message);
  },

  // Handle errors
  onError: (error) => {
    console.error('Chat error:', error);
  },
});
```

## Headers

To pass required headers (validated against your Context Config), include them as custom headers:

```tsx
const { messages, sendMessage } = useChat({
  api: 'http://localhost:3002/run/api/chat',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    // Custom context headers
    'x-user-id': 'user-123',
    'x-organization-id': 'org-456',
  },
});
```

<Note>
  See [Headers](/typescript-sdk/headers) for details.
</Note>

## Complete Example

Here's a more complete example with conversation management and custom styling:

```tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";

export default function Page() {
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: "{process.env.NEXT_PUBLIC_INKEEP_AGENTS_API_URL}/run/api/chat",
      headers: {
        "x-inkeep-tenant-id": "default",
        "x-inkeep-project-id": "activities-planner",
        "x-inkeep-agent-id": "activities-planner",
        tz: "US/Pacific",
      },
    }),
  });
  const [input, setInput] = useState("");

  return (
    <div className="max-w-2xl mx-auto p-4">
      <div className="space-y-4 mb-4">
        {messages.map((message) => (
          <div key={message.id} className="border rounded p-3">
            <div className="font-semibold mb-2">
              {message.role === "user" ? "👤 User" : "🤖 Assistant"}
            </div>

            <div className="space-y-2">
              {message.parts.map((part, partIndex) => {
                const partKey = `${message.id}-${part.type}-${partIndex}`;

                if (part.type === "text") {
                  return (
                    <div key={partKey} className="whitespace-pre-wrap">
                      {part.text}
                    </div>
                  );
                }

                if (part.type?.startsWith("data-")) {
                  return (
                    <div
                      key={partKey}
                      className="bg-blue-50 p-3 rounded border-l-4 border-blue-400"
                    >
                      <div className="font-semibold text-blue-700">
                        Data: {JSON.stringify(part, null, 2)}
                      </div>
                    </div>
                  );
                }
                return null;
              })}
            </div>
          </div>
        ))}
      </div>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (input.trim()) {
            sendMessage({ text: input });
            setInput("");
          }
        }}
        className="flex gap-2"
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Ask about weather, request confirmation, or ask for location..."
          className="flex-1 p-2 border rounded"
        />
        <button
          type="submit"
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          Send
        </button>
      </form>

      <div className="mt-4 text-sm text-gray-600">
        <p>Try asking:</p>
        <ul className="list-disc list-inside space-y-1">
          <li>"What's the weather in New York?"</li>
          <li>"Can you get my location?"</li>
          <li>"Ask me to confirm something"</li>
        </ul>
      </div>
    </div>
  );
}
```

## Environment Variables

For production deployments, store configuration in environment variables:

```bash
NEXT_PUBLIC_INKEEP_AGENTS_API_URL=your_domain_for_run_api
NEXT_PUBLIC_INKEEP_API_KEY=your-api-key
NEXT_PUBLIC_TENANT_ID=your-tenant-id
NEXT_PUBLIC_PROJECT_ID=your-project-id
NEXT_PUBLIC_AGENT_ID=your-agent-id
```

<Note>
  In local development, the Agents API runs at `http://localhost:3002`. For production, update this to your deployed API URL.
</Note>

## Next Steps

<Cards>
  <Card title="AI Elements" icon="LuPackage" href="/talk-to-your-agents/vercel-ai-sdk/ai-elements">
    Explore Vercel's prebuilt UI components library built on shadcn/ui for rapid development.
  </Card>

  <Card title="React UI Components" icon="LuLayers" href="/talk-to-your-agents/react/chat-button">
    Use prebuilt React components for a complete chat UI experience.
  </Card>

  <Card title="Via API" icon="LuNetwork" href="/talk-to-your-agents/chat-api">
    Learn about the underlying HTTP API and streaming protocol.
  </Card>
</Cards>


# Environment-aware Credentials (/typescript-sdk/credentials/env-aware-credentials)

Automatically load environment-specific credentials and switch between development and production configurations.



Environment-aware credentials enable automatic loading of environment-specific credentials (development, production, etc.) while keeping them cleanly separated and easily switchable via the CLI's --env flag.

Credentials are referenced in the TypeScript SDK using the `envSettings` object. Creating environment-aware credentials is a two step process:

### Step 1: Define environment configurations

<Tabs>
  <Tab title="Development Config">
    ```typescript title="projects/my-project/environments/development.env.ts"
    import { registerEnvironmentSettings } from '@inkeep/agents-sdk';
    import { CredentialStoreType } from '@inkeep/agents-core';

    // How credentials are referenced depends on how the credential was created, in this case this credential was created as an environment variable
    export const development = registerEnvironmentSettings({
    credentials: {
    'stripe_api_key': {
      id: 'stripe-api-key',
      name: 'Stripe API Key',
      type: CredentialStoreType.memory,
      credentialStoreId: 'memory-default',
      retrievalParams: {
        key: 'STRIPE_API_KEY_DEV',
      },
    }
    },
    });
    ```
  </Tab>

  <Tab title="Production Config">
    ```typescript title="projects/my-project/environments/production.env.ts"
    import { registerEnvironmentSettings } from '@inkeep/agents-sdk';
    import { CredentialStoreType } from '@inkeep/agents-core';

    // How credentials are referenced depends on how the credential was created, in this case this credential was created as an environment variable
    export const production = registerEnvironmentSettings({
    credentials: {
    'stripe_api_key': {
      id: 'stripe-api-key',
      name: 'Stripe API Key',
      type: CredentialStoreType.memory,
      credentialStoreId: 'memory-default',
      retrievalParams: {
        key: 'STRIPE_API_KEY_PROD',
      },
    }
    },
    });
    ```
  </Tab>

  <Tab title="Index File">
    ```typescript title="projects/my-project/environments/index.ts"
    import { createEnvironmentSettings } from '@inkeep/agents-sdk';
    import { development } from './development.env';
    import { production } from './production.env';

    export const envSettings = createEnvironmentSettings({
    development,
    production,
    });
    ```
  </Tab>
</Tabs>

### Step 2: Use the credentials in tools

```typescript title="<your-project-name>/tools/stripe-tool.ts"
import { mcpTool } from "@inkeep/agents-sdk";
import { envSettings } from "../environments";

export const stripeTool = mcpTool({
  id: 'stripe-tool',
  name: 'Stripe',
  serverUrl: 'https://mcp.stripe.com/mcp',
  credential: envSettings.getEnvironmentCredential('stripe_api_key'),
});
```

This pattern is useful if you want to keep track of different credentials for different environments. When you push your project using the [Inkeep CLI](/typescript-sdk/cli-reference#inkeep-push) `inkeep push` command with the `--env` flag, the credentials will be loaded from the appropriate environment file. For example, if you run `inkeep push --env development`, the credentials will be loaded from the `environments/development.env.ts` file.

### CLI Environment Variables

The CLI respects these environment variables when using the `--env` flag:

```bash
# Set environment name via environment variable
export INKEEP_ENV=production
inkeep push  # Uses production environment automatically

# Override via CLI (takes precedence)
inkeep push --env development  # Uses development instead
```


# Use Environment Variables as Credentials (/typescript-sdk/credentials/environment-variables)

Store simple API keys and bearer tokens using environment variables in development or production.



Environment variables are a simple way to store credentials in your environment. They are stored in the environment variables in your `.env` file.

## Referencing credentials in SDK

Here is an example of how to reference a credential in the TypeScript SDK for Bearer Authentication.

```typescript
import { agent, subAgent, mcpTool, credential } from "@inkeep/agents-sdk";
import { CredentialStoreType } from "@inkeep/agents-core";

// Step 1: Reference the credential created in the environment file
const stripeCredential = credential({
  id: 'stripe-credential',
  name: 'Stripe Credential',
  type: CredentialStoreType.memory,
  credentialStoreId: 'memory-default',
  retrievalParams: {
    "key": "<environment-variable-name>", // where <environment-variable-name> is the name of the environment variable that contains the credential value, for example, STRIPE_API_KEY
  },
});

// Step 2: Use credential in MCP tool
const stripeTool = mcpTool({
  id: 'stripe-tool',
  name: 'Stripe Tool',
  description: 'Access Stripe payment services',
  serverUrl: 'https://mcp.stripe.com/mcp',
  credential: stripeCredential,
});
```

### Credential Configuration

| Parameter           | Type                | Required | Description                                              |
| ------------------- | ------------------- | -------- | -------------------------------------------------------- |
| `id`                | string              | Yes      | Unique identifier for the credential                     |
| `type`              | CredentialStoreType | Yes      | Type of credential store (memory, nango, or keychain)    |
| `credentialStoreId` | string              | Yes      | Identifier for the specific credential store instance    |
| `retrievalParams`   | object              | Yes      | Parameters for retrieving the credential from the store. |


# Use Keychain for Local Development (/typescript-sdk/credentials/keychain)

Use the local keychain to store OAuth tokens and bearer credentials during development.



Keychain is the default method for storing credentials for every project. It is a locally stored credential store for OAuth tokens from OAuth2.1/PKCE flows and Bearer authentication, configured through Visual Builder and referenced in the TypeScript SDK.

<Warning>
  Keychain lacks automatic token refresh, production suitability, and metadata header support, making Nango the recommended solution for production-ready credential management. See setup instructions for Nango [here](/typescript-sdk/credentials/nango).
</Warning>

## Referencing credentials in SDK

Here is an example of how to reference a credential in the TypeScript SDK for both Bearer Authentication and OAuth2.1/PKCE.

<Tabs>
  <Tab title="Bearer Authentication">
    ```typescript
    import { agent, subAgent, mcpTool, credential } from "@inkeep/agents-sdk";
    import { CredentialStoreType } from "@inkeep/agents-core";

    // Step 1: Reference the credential created in the Visual Builder
    const stripeCredential = credential({
      id: 'stripe-credential',
      name: 'Stripe Credential',
      type: CredentialStoreType.memory,
      credentialStoreId: 'memory-default',
      retrievalParams: {
        "key": "<id-given-to-bearer-api-key>" // where <id-given-to-bearer-api-key> is the id given to the bearer api key created through the Visual Builder
      },
    });

    // Step 2: Use credential in MCP tool
    const stripeTool = mcpTool({
      id: 'stripe-tool',
      name: 'Stripe Tool',
      description: 'Access Stripe payment services',
      serverUrl: 'https://mcp.stripe.com/mcp',
      credential: stripeCredential,
    });
    ```
  </Tab>

  <Tab title="OAuth2.1/PKCE">
    ```typescript
    import { agent, subAgent, mcpTool, credential } from "@inkeep/agents-sdk";
    import { CredentialStoreType } from "@inkeep/agents-core";

    // Step 1: Reference the credential created in the Visual Builder
    const stripeCredential = credential({
      id: 'stripe-credential',
      name: 'Stripe Credential',
      type: CredentialStoreType.memory, // Memory store
      credentialStoreId: 'memory-default',
      retrievalParams: {
        "key": "oauth_token_<toolId>" // where <toolId> is the id of the MCP tool the credential is associated with
      },
    });

    // Step 2: Use credential in MCP tool
    const stripeTool = mcpTool({
      id: 'stripe-tool',
      name: 'Stripe Tool',
      description: 'Access Stripe payment services',
      serverUrl: 'https://mcp.stripe.com/mcp',
      credential: stripeCredential,
    });
    ```
  </Tab>
</Tabs>

### Credential Configuration

| Parameter           | Type                | Required | Description                                              |
| ------------------- | ------------------- | -------- | -------------------------------------------------------- |
| `id`                | string              | Yes      | Unique identifier for the credential                     |
| `type`              | CredentialStoreType | Yes      | Type of credential store (memory, nango, or keychain)    |
| `credentialStoreId` | string              | Yes      | Identifier for the specific credential store instance    |
| `retrievalParams`   | object              | Yes      | Parameters for retrieving the credential from the store. |


# Use Nango as a Credential Store (/typescript-sdk/credentials/nango)

Set up Nango as an OAuth-capable credential store for your Inkeep agents.



Nango is an excellent choice for both development and production environments, particularly when working with OAuth2.1/PKCE flows and complex integrations.

To get started, first set up Nango either through cloud or local deployment, then create your credential, and finally consume the credential through the TypeScript SDK.

## Nango setup option 1: Nango Cloud

### Step 1: Create a Nango account

Sign up [here](https://app.nango.dev/signin).

### Step 2: Save your Nango secret key

After creating your Nango account, navigate to Environment Settings in your dashboard and copy the secret key.

### Step 3: Configure your root `.env` file

```
NANGO_SECRET_KEY=your_nango_secret_key
```

Restart your dev server to load the new environment variable:

```bash
pnpm dev
```

### Step 4: Create a new credential in the Visual Builder using Nango

<Video src="/videos/create-nango-credential.mp4" />

## Nango setup option 2: Nango local

### Automated Setup (Recommended)

<Note>
  Run `pnpm setup-dev` first to set up core databases and migrations. `setup-dev:optional` only starts optional services.
</Note>

Run the following from your project root:

```bash
pnpm setup-dev:optional
```

This:

* Clones [`agents-optional-local-dev`](https://github.com/inkeep/agents-optional-local-dev) into `.optional-services/` if not already present
* Starts Nango, [SigNoz](/get-started/traces), OTEL Collector, and Jaeger via Docker Compose
* Generates a Nango secret key and writes it to your `.env` (re-uses an existing key if already set)
* Sets `NANGO_SERVER_URL`, `PUBLIC_NANGO_SERVER_URL`, and `PUBLIC_NANGO_CONNECT_BASE_URL`

Restart your dev server after setup completes:

```bash
pnpm dev
```

Then create a credential in the [Visual Builder](/visual-builder/tools/credentials) using Nango Store.

<Video src="/videos/create-nango-credential.mp4" />

Use `pnpm optional:status` to check service health, `pnpm optional:stop` to stop optional services, or `pnpm optional:reset` to start fresh.

### Manual Setup

If you prefer manual setup:

#### Step 1: Clone the optional services repository

```bash
git clone https://github.com/inkeep/agents-optional-local-dev .optional-services
cd .optional-services
```

#### Step 2: Configure the `.optional-services/.env`

Nango requires an encryption key for credential storage. Create the `.env` from the template:

```bash
cp .env.docker.example .env && \
  nango_encryption_key=$(openssl rand -base64 32) && \
  nango_dashboard_password=$(openssl rand -base64 8) && \
  tmp_file=$(mktemp) && \
  sed \
    -e "s|<REPLACE_WITH_NANGO_ENCRYPTION_KEY>|$nango_encryption_key|" \
    -e "s|<REPLACE_WITH_NANGO_DASHBOARD_PASSWORD>|$nango_dashboard_password|" \
    .env > "$tmp_file" && \
  mv "$tmp_file" .env && \
  echo ".env created with auto-generated NANGO_ENCRYPTION_KEY and NANGO_DASHBOARD_PASSWORD"
```

<Warning>
  Once set, 

  `NANGO_ENCRYPTION_KEY`

   cannot be changed without losing existing encrypted data.
</Warning>

#### Step 3: Start Nango Services

Inside `.optional-services`, start Nango:

```bash
docker compose --profile nango up -d
```

#### Step 4: Save your Nango secret key

1. Open Nango at `http://localhost:3050`
2. Navigate to Environment Settings and copy the secret key

#### Step 5: Configure your root `.env` file

In your **root project directory** (not inside `.optional-services/`), update your `.env` file:

```bash
NANGO_SECRET_KEY=your_nango_secret_key
NANGO_SERVER_URL=http://localhost:3050
PUBLIC_NANGO_SERVER_URL=http://localhost:3050
PUBLIC_NANGO_CONNECT_BASE_URL=http://localhost:3051
```

Restart your dev server:

```bash
pnpm dev
```

#### Step 6: Create a credential in the Visual Builder

Create a credential in the [Visual Builder](/visual-builder/tools/credentials) using Nango Store.

<Video src="/videos/create-nango-credential.mp4" />

## Referencing credentials in SDK

Once you have created a credential in the Visual Builder, you can reference it in the TypeScript SDK for both Bearer Authentication and OAuth2.1/PKCE.

Here is an example of how to reference a credential in the TypeScript SDK for both Bearer Authentication and OAuth2.1/PKCE.

<Tabs>
  <Tab title="Bearer Authentication">
    ```typescript
    import { agent, subAgent, mcpTool, credential } from "@inkeep/agents-sdk";
    import { CredentialStoreType } from "@inkeep/agents-core";

    // Step 1: Reference the credential created in the Visual Builder
    const stripeCredential = credential({
      id: 'stripe-credential',
      name: 'Stripe Credential',
      type: CredentialStoreType.memory,
      credentialStoreId: 'memory-default',
      retrievalParams: {
        "connectionId": "<id-given-to-bearer-api-key>", // where <id-given-to-bearer-api-key> is the id given to the bearer api key created through the Visual Builder
        "providerConfigKey": "<id-given-to-bearer-api-key>",
        "provider": "private-api-bearer",
        "authMode": "API_KEY"
      },
    });

    // Step 2: Use credential in MCP tool
    const stripeTool = mcpTool({
      id: 'stripe-tool',
      name: 'Stripe Tool',
      description: 'Access Stripe payment services',
      serverUrl: 'https://mcp.stripe.com/mcp',
      credential: stripeCredential,
    });
    ```
  </Tab>

  <Tab title="OAuth2.1/PKCE">
    ```typescript
    import { agent, subAgent, mcpTool, credential } from "@inkeep/agents-sdk";
    import { CredentialStoreType } from "@inkeep/agents-core";

    // Step 1: Reference the credential created in the Visual Builder
    const stripeCredential = credential({
      id: 'stripe-credential',
      name: 'Stripe Credential',
      type: CredentialStoreType.memory, // Memory store
      credentialStoreId: 'memory-default',
      retrievalParams: {
        "connectionId": "<nango-connection-id>", // Nango connection ID that was generated. You can confirm this in your nango dashboard (cloud or self-hosted)
        "providerConfigKey": "<mcp-server-name>_<mcp-server-generated-id>", // For example, linear_AABBCC1234
        "provider": "mcp-generic",
        "authMode": "OAUTH2"
      },
    });

    // Step 2: Use credential in MCP tool
    const stripeTool = mcpTool({
      id: 'stripe-tool',
      name: 'Stripe Tool',
      description: 'Access Stripe payment services',
      serverUrl: 'https://mcp.stripe.com/mcp',
      credential: stripeCredential,
    });
    ```
  </Tab>
</Tabs>

### Credential Configuration

| Parameter           | Type                | Required | Description                                              |
| ------------------- | ------------------- | -------- | -------------------------------------------------------- |
| `id`                | string              | Yes      | Unique identifier for the credential                     |
| `type`              | CredentialStoreType | Yes      | Type of credential store (memory, nango, or keychain)    |
| `credentialStoreId` | string              | Yes      | Identifier for the specific credential store instance    |
| `retrievalParams`   | object              | Yes      | Parameters for retrieving the credential from the store. |


# Credential Store Options (/typescript-sdk/credentials/overview)

Choose a credential store for securely managing secrets in development and production.



<SkillRule id="credential-store-options" skills="typescript-sdk" title="Credential Store Options" description="Options for storing MCP server and external agent credentials">
  ## Credential Store Options

  MCP servers and external agents may require authentication for secure access. The Inkeep agent framework supports storing these credentials in three different ways:

  <OptionCards>
    <OptionCard title="Nango Store" icon="brand/Nango" href="/typescript-sdk/credentials/nango" badge="Recommended" highlighted cta="Learn More">
      **Best for:** Development or Production environments with OAuth2.1/PKCE flows and complex integrations

      * Automatic token refresh for OAuth
      * Supports additional metadata headers
      * Works with complex OAuth flows (OAuth2.0/PKCE)
      * Managed service (self-hosted or cloud)
    </OptionCard>

    <OptionCard title="Keychain Store" icon="LuLock" href="/typescript-sdk/credentials/keychain" badge="Default" cta="Learn More">
      **Best for:** Local development with OAuth services

      * Not suitable for production
      * No automatic token refresh
      * Requires manual re-authentication when tokens expire
      * Does not support additional metadata headers
    </OptionCard>

    <OptionCard title="Environment Variables" icon="LuDatabase" href="/typescript-sdk/credentials/environment-variables" cta="Learn More">
      **Best for:** Simple API keys and bearer tokens in development or production

      * Direct configuration via TypeScript SDK
      * Does not support additional metadata headers
      * Does not support OAuth2.1/PKCE flows
    </OptionCard>
  </OptionCards>

  ## Environment-aware Credentials

  When you need different credentials for different environments (e.g., development vs. production), you can take advantage of [environment-aware credentials](/typescript-sdk/credentials/env-aware-credentials). This approach allows you to:

  * Define separate credentials for each environment
  * Automatically load the correct credentials based on your deployment environment
  * Keep your development and production credentials cleanly separated
  * Easily switch between environments using the CLI's `--env` flag

  Learn more about setting up environment-aware credentials in the [dedicated guide](/typescript-sdk/credentials/env-aware-credentials).
</SkillRule>


# Artifact Components in TypeScript SDK (/typescript-sdk/structured-outputs/artifact-components)

Learn how to use artifact components to store and retrieve data from the agent



<>
  Artifacts are structured components that capture and store source information when agents interact with tools or other agents. They act as a record-keeping system, automatically documenting where information comes from during tool and agent interactions.

  ## How Artifacts Work

  When an agent uses a tool (whether that's another agent or a utility tool), the response is automatically parsed to create artifacts. These artifacts store:

  * The source of the information (tool/agent used)
  * The relevant content from the response
  * Metadata about the interaction

  ## Oversized Artifacts

  When tool results exceed **30% of the model's context window**, they're marked as **oversized** to prevent context overflow.

  **What happens:**

  * ✅ Full data is persisted to storage
  * ❌ Retrieval is blocked to preserve context space
  * ⚠️ Visual warnings appear in the UI traces panel

  **UI Indicators:**

  * Amber warning badge in timeline: `⚠️ Oversized artifact (~247K tokens)`
  * Detailed warning in expanded view with size and percentage of context

  **Tracing Attributes:**

  * `artifact.is_oversized` - Boolean flag
  * `artifact.retrieval_blocked` - Whether retrieval is blocked
  * `artifact.original_token_size` - Estimated token count
  * `artifact.context_window_size` - Model's context limit

  **Troubleshooting:**
  If seeing many oversized artifacts, consider:

  * Filter tool outputs to return only necessary fields
  * Paginate large result sets
  * Pre-summarize responses in tools
  * Use models with larger context windows
</>

<SkillRule id="artifact-components-definition" skills="typescript-sdk" title="Defining Artifact Components" description="How to define artifact components with preview fields for citations and sources">
  ## Defining Artifact Components

  Artifact components are defined using the `artifactComponent` builder function. **We recommend using Zod schemas** for type safety and better developer experience.

  <Callout>
    **Quick Start**: Citation artifacts are automatically included when you create
    a new project with `npx @inkeep/create-agents`, providing a ready-to-use
    example.
  </Callout>

  ```typescript
  import { artifactComponent } from "@inkeep/agents-sdk";
  import { z } from "zod";
  import { preview } from "@inkeep/agents-core";

  export const citation = artifactComponent({
    id: "citation",
    name: "citation",
    description: "Structured factual information extracted from search results",
    props: z.object({
      title: preview(z.string().describe("Title of the source document")),
      url: preview(z.string().describe("URL of the source document")),
      record_type: preview(
        z.string().describe("Type of record (documentation, blog, guide, etc.)")
      ),
      content: z
        .array(
          z.object({
            type: z
              .string()
              .describe("Type of content (text, image, video, etc.)"),
            text: z.string().describe("The actual text content"),
          })
        )
        .describe(
          "Array of structured content blocks extracted from the document"
        ),
    }),
  });
  ```

  <Callout>
    JSON Schema is also supported. Set `inPreview: true` on properties instead of
    using `preview()`.
  </Callout>

  ## Schema Requirements

  <Callout type="warning">
    **Critical**: Artifact schemas must match your tool's output structure. The
    system uses JMESPath selectors on JSON - it **cannot extract from free text**.
  </Callout>

  ```typescript
  // ❌ WRONG: Tool returns text, schema expects structured data
  // Tool output: { result: { content: [{ text: "Weather: Sunny, 75°F" }] } }
  props: z.object({
    temperature: z.number(), // ❌ Cannot extract number from text
    condition: z.string(), // ❌ Cannot extract condition from text
  });

  // ✅ CORRECT: Tool returns structured data matching schema
  // Tool output: { result: { temperature: 75, condition: "Sunny" } }
  props: z.object({
    temperature: z.number(),
    condition: z.string(),
  });
  ```

  Use OpenTelemetry traces to debug schema validation issues.

  ## Preview Fields

  Use `preview()` to mark fields for immediate availability. The `preview()` helper automatically sets `inPreview: true` in the generated schema.

  **Preview fields are:**

  * Included in the agent's context when an artifact is referenced, so the agent can reason about them directly
  * Shown to other agents in the project for cross-agent reasoning
  * Streamed in real-time to clients (Vercel AI SDK)
  * Auto-rendered by Inkeep's widget (citations as interactive cards)
  * Available immediately in UI (full artifact loads on-demand)

  **Non-preview fields** are persisted in storage but kept out of the agent's working context by default, keeping the context window lean. Agents can retrieve the complete artifact — including all non-preview fields — on demand when they need it.

  **No schema?** Omit `props` to save the entire tool result without filtering.

  ## Accessing Artifact Data

  Agents can access artifact data in a few ways:

  * **In context** — after creating or referencing an artifact, the agent can see its preview fields directly in its working context
  * **Passing to a tool** — the agent can supply an artifact as an argument to a tool; the tool receives the complete artifact data, including all non-preview fields
  * **On-demand retrieval** — the agent can explicitly fetch the full artifact when it needs to read non-preview fields itself

  Not every intermediate tool result needs to become an artifact. When an agent chains tool calls — passing one tool's output directly into the next — no artifact is created and nothing surfaces to the user. See [Tool Output Pipelines](/typescript-sdk/tools/function-tools#tool-output-pipelines).

  ## Passing Artifacts to Tools

  When an agent passes an artifact to a tool as an argument, the tool receives the **complete artifact data** — all fields, including those not marked `inPreview`. This means you can design tools that work with rich artifact content without needing to fetch or reconstruct data manually.

  For example, a document processing tool can access the full `content` array from a citation artifact even though `content` is a non-preview field:

  ```typescript
  import { functionTool } from "@inkeep/agents-sdk";
  import { z } from "zod";

  const summarizeTool = functionTool({
    id: "summarize-document",
    description: "Summarize the content of a document artifact",
    inputSchema: z.object({
      // The agent passes an artifact here; the tool receives all fields
      document: z.object({
        title: z.string(),
        url: z.string(),
        content: z.array(z.object({ type: z.string(), text: z.string() })),
      }),
    }),
    execute: async ({ document }) => {
      // document.content is fully available here, even though it's non-preview
      const fullText = document.content.map((c) => c.text).join("\n");
      return { summary: fullText.slice(0, 500) };
    },
  });
  ```

  ## Artifact Creation

  When an agent uses a tool:

  1. The tool processes the request and returns a response
  2. **The agent decides** whether to create an artifact from the tool response
  3. If created, the artifact is parsed according to the artifact component schema
  4. The artifact can be associated with any data components that reference this information

  ## Artifacts vs Data Components

  **Artifacts** and **Data Components** are different:

  * **Artifacts**: Citations/sources from tool results stored in the database
  * **Data Components**: UI elements agents output in responses (Text, Fact, etc.)

  Artifacts provide the source citations that back up information in data components.

  ## Using in Agents

  After defining your artifact component, use it in an agent:

  ```typescript
  import { agent } from "@inkeep/agents-sdk";
  import { citation } from "./artifact-components/citation";

  const searchAgent = agent({
    id: "search-agent",
    canUse: () => [searchTool],
    prompt: "Search for information and cite sources using artifacts.",
    artifactComponents: () => [citation],
  });
  ```

  ## Citation Artifacts

  Citation artifacts that are named `citation` and have `title` and `url` preview fields are automatically rendered as interactive cards by Inkeep's widget library. This provides source attribution and allows users to verify information at the source.

  <Image src="/images/citation-widget-example.png" alt="Citation artifact rendered as an interactive card in the Inkeep widget showing source attribution" style={{ marginTop: "1rem" }} />

  ## Example Flow

  Here's a complete example with inline artifact component definition:

  ```typescript
  import { agent } from "@inkeep/agents-sdk";
  import { z } from "zod";
  import { preview } from "@inkeep/agents-core";

  const searchAgent = subAgent({
    id: "search",
    canUse: () => [searchTool],
    prompt: "Search for information and cite sources using artifacts.",
    artifactComponents: [
      {
        id: "citation",
        name: "citation",
        description: "Search result document",
        props: z.object({
          title: preview(z.string().describe("Document title")),
          url: preview(z.string().describe("Document URL")),
          content: z.string().describe("Document content"),
        }),
      },
    ],
  });
  ```

  ## No Schema Example

  ```typescript
  const rawDataAgent = subAgent({
    id: "raw-data",
    canUse: () => [apiTool],
    prompt: "Fetch data and save complete responses.",
    artifactComponents: [
      {
        id: "api-response",
        name: "API Response",
        description: "Complete API response data",
        // No props - saves entire tool result
      },
    ],
  });
  ```

  Example flow:

  1. Agent uses search tool and gets results
  2. **Agent decides** to create an artifact citing the source document
  3. Artifact is stored with preview fields (title, url) shown to other agents
  4. Other agents can reference this artifact or get full content when needed

  ## Enforcing artifact emission

  By default a sub-agent that declares artifact components *may* create them. To require that a sub-agent produces or references a specific artifact on every turn, see the [Output Contract](/typescript-sdk/structured-outputs/output-contract).
</SkillRule>


# Data Components in TypeScript SDK (/typescript-sdk/structured-outputs/data-components)

Define data component schemas for rich, interactive agent messages using the TypeScript SDK.



<>
  ## What are Data Components?

  Data Components allow your agents to return rich, interactive content instead of just text. Think of them as reusable UI building blocks that your agent can populate with data.

  For example, instead of returning "You have 3 tasks: Task #123 (shipped), Task #124 (processing)...", your agent can return a properly formatted task list component.
</>

<SkillRule id="data-components-definition" skills="typescript-sdk" title="Defining Data Components" description="How to define data components using Zod schemas">
  ## Defining Data Components

  Data components are defined using the `dataComponent` builder function. **We recommend using Zod schemas** for type safety and better developer experience.

  ```typescript
  import { dataComponent } from '@inkeep/agents-sdk';
  import { z } from 'zod';

  export const taskList = dataComponent({
    id: "task-list",
    name: "TaskList",
    description: "Display user tasks with status",
    props: z.object({
      tasks: z
        .array(
          z.object({
            id: z.string().describe("Unique task identifier"),
            title: z.string().describe("Task title"),
            completed: z.boolean().describe("Whether the task is completed"),
            priority: z.enum(["low", "medium", "high"]).describe("Task priority"),
          })
        )
        .describe("Array of user tasks"),
      totalCount: z.number().describe("Total number of tasks"),
      completedCount: z.number().describe("Number of completed tasks"),
    }),
  });
  ```

  <Callout>
    JSON Schema is also supported if you prefer traditional schema definitions.
  </Callout>

  ## Using in Agents

  Register data components with your agent:

  ```typescript
  import { agent } from '@inkeep/agents-sdk';
  import { taskList } from './data-components/task-list';

  const taskAgent = agent({
    id: "task-agent",
    name: "Task Agent",
    prompt: "Provide helpful answers and use data components when appropriate.",
    dataComponents: () => [taskList],
  });
  ```

  When a user asks "Show me my tasks", the agent will respond with:

  ```json
  {
    "dataComponents": [
      {
        "id": "task-list",
        "name": "TaskList",
        "props": {
          "tasks": [
            { "id": "1", "title": "Review documentation", "completed": false },
            { "id": "2", "title": "Update website", "completed": true }
          ]
        }
      }
    ]
  }
  ```

  ## Response Shape

  A response from a `dataComponents`-enabled agent can take three shapes, exposed on the `agent_generate` data operation as `generationType`:

  * **`object_generation`** — only data components, no prose (the example above).
  * **`text_generation`** — only text. The framework returns the model's prose if it does not produce a valid structured object, so users see the response instead of a blank message.
  * **`mixed_generation`** — text and one or more data components in the same message. `parts` is ordered by emission, so a reasoning prelude can precede a component, narration can sit between components, or commentary can follow them.

  The `agent_generate` event records `parts` with `type: 'text' | 'data_component' | 'data_artifact'` (plus `'tool_call' | 'tool_result'` for internal tool events). Iterate `parts` in order and render each entry by its `type`. See [Data Operations](/typescript-sdk/data-operations#agent_generate) for the full event shape.

  ## Frontend Integration

  Create a React component that receives the props defined by your data component schema:

  ```tsx
  const TaskList = ({ tasks, totalCount, completedCount }) => (
    <div className="task-list">
      <h3>Your Tasks ({completedCount}/{totalCount} completed)</h3>
      {tasks.map((task) => (
        <div key={task.id} className={task.completed ? "completed" : ""}>
          {task.title}
        </div>
      ))}
    </div>
  );
  ```

  Register the component with the chat widget.

  ```tsx
  import { TaskList } from './ui/TaskList';

  <InkeepSidebarChat
    aiChatSettings={{
      baseUrl: "your-api-server-url",
      appId: "your-app-id",
      components: {
        "TaskList": TaskList, // Key = component name; value = your component
      },
    }}
  />
  ```

  The agent will automatically return structured data that renders as your React component.

  ## Enforcing component emission

  By default a sub-agent that declares data components *may* use them. To require that a sub-agent emits a specific data component on every turn — or forbid it from narrating in plain text — see the [Output Contract](/typescript-sdk/structured-outputs/output-contract).
</SkillRule>


# Output Contract (/typescript-sdk/structured-outputs/output-contract)

Constrain a sub-agent to emit structured output only, instead of free-text narration



## Overview

A sub-agent's `outputContract` lets you declare — and have the framework enforce — that the sub-agent emits **structured output only**, rather than relying on prompt discipline.

This is most useful for multi-stage pipelines (for example `query → select → respond`) where the intermediate stages should not narrate to the user. By default the framework always offers the model a "write prose" option, so a stage will narrate regardless of its prompt. The output contract removes that option.

The contract is **opt-in**: a sub-agent without an `outputContract` behaves exactly as before.

<Note>
  Using the Visual Builder? The same contract is configured visually in a sub agent's [Guardrails](/visual-builder/structured-outputs/output-contract) section.
</Note>

## The contract

```ts
import { subAgent } from '@inkeep/agents-sdk';

const query = subAgent({
  id: 'query',
  name: 'Query',
  description: 'Searches and returns structured results',
  prompt: 'Search the knowledge base and return results.',
  dataComponents: () => [searchResults],
  outputContract: {
    allowText: false,
    requireComponent: ['SearchResults'],
    onViolation: 'reject',
  },
});
```

| Field              | Type                 | Meaning                                                                                                                                                                                                                          |
| ------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowText`        | `boolean`            | Whether the sub-agent may emit free text. Defaults to `true`. Set to `false` to forbid prose.                                                                                                                                    |
| `requireComponent` | `string[]`           | The response must include **every** listed [data component](/typescript-sdk/structured-outputs/data-components) (require-all).                                                                                                   |
| `requireArtifact`  | `string[]`           | The response must create or reference **every** listed [artifact component](/typescript-sdk/structured-outputs/artifact-components) (require-all).                                                                               |
| `requireTransfer`  | `boolean`            | When `true`, the response must [transfer](/typescript-sdk/agent-relationships) to another sub-agent. `canTransferTo()` constrains where. Mutually exclusive with `requireComponent` and `requireArtifact` — a turn ends one way. |
| `onViolation`      | `'reject' \| 'warn'` | What happens when the contract is violated. Defaults to `reject`.                                                                                                                                                                |

`allowText` and the `require*` fields are independent. `{ allowText: true, requireComponent: ['X'] }` means narration is permitted, but component `X` must also appear. `{ allowText: false, requireComponent: ['X'] }` means component `X` must appear and prose is forbidden.

## On violation

When a contract is violated:

* **`reject`** (default) — the run fails with a structured contract-violation error.
* **`warn`** — the violation is logged and the response is surfaced as it is today (observe-only).

### Handling a rejection

A `reject` surfaces as a structured signal you can branch on, not a raw stack trace:

* The run's task fails with a `contract_violation` error type.
* Streamed runs emit an error operation with `code: 'CONTRACT_VIOLATION'`.

The error message names the sub-agent, the applied policy, and the specific rule that was violated (for example "requireComponent — must emit SearchResults but emitted Other"), so a consumer (chat widget, SDK caller, playground) can distinguish a contract rejection from an ordinary execution failure and surface a useful message.

## Validation

Every entry of `requireComponent` and `requireArtifact` must name a data component or artifact component the sub-agent declares. `requireTransfer: true` requires the sub-agent to declare at least one `canTransferTo()` target. Mismatches are caught at registration time with a clear error.

## Notes

* `requireArtifact` is satisfied either by **creating** the named artifact on the turn, or by **referencing** an existing artifact of that type.
* Delegates and tool calls are still allowed under `allowText: false` — they're how a sub-agent does its work — but cannot be the subject of a `require*` field.
* `requireTransfer: true` cannot be combined with `requireComponent` or `requireArtifact`. A turn ends one way: a transfer or a structured component/artifact, not both. The combination is rejected at registration time.


# Status Updates (/typescript-sdk/structured-outputs/status-updates)

Real-time progress updates powered by AI that keep users informed during long-running agent operations



import { Callout } from "fumadocs-ui/components/callout";

The Inkeep Agent Framework provides intelligent, AI-powered status updates that automatically generate contextual progress messages during agent execution. These updates give users ChatGPT-style feedback about what's happening behind the scenes without exposing internal technical details.

## Overview

Status updates are automatically generated summaries that:

* **Track Progress**: Monitor agent operations, tool calls, and task completions
* **Provide Context**: Generate user-friendly descriptions of what's happening
* **Hide Complexity**: Abstract away internal agent names and technical operations
* **Stream Real-time**: Send updates via the same streaming interface as responses

<Callout type="info">
  Status updates use your Agent's summarizer model to generate contextual,
  intelligent progress messages that feel natural to users.
</Callout>

## How It Works

The status updater operates through three main components:

### 1. Agent Session Event Tracking

Every agent operation is tracked as events:

* **agent\_generate**: Text generation, structured output, or a mix of both
* **transfer**: Agent-to-agent transfers
* **delegation\_sent/returned**: Task delegation between agents
* **tool\_call**: When a tool is called with arguments
* **tool\_result**: When a tool execution completes (success or failure)
* **error**: System-level errors and configuration issues
* **artifact\_saved**: Document or data artifacts created

### 2. Intelligent Triggering

Status updates are triggered by:

* **Event Count**: After N events (tool calls, generations, etc.)
* **Time Intervals**: Every N seconds of processing
* **Smart Detection**: Avoids updates during text streaming

### 3. AI-Powered Summarization

The summarizer model:

* Analyzes recent events and progress
* Generates user-friendly status messages
* Maintains context across multiple updates
* Focuses on user-visible outcomes

<SkillRule id="status-updates-config" skills="typescript-sdk" title="Status Updates Configuration" description="How to configure AI-powered status updates for agents">
  ## Configuration

  Configure status updates in your agent:

  ```typescript
  export const myAgent = agent({
    id: "my-agent",
    defaultSubAgent: myAgent,
    subAgents: () => [myAgent],

    // Model Settings with summarizer
    models: {
      base: { model: "anthropic/claude-sonnet-4-5" },
      summarizer: { model: "anthropic/claude-sonnet-4-5" }, // Model for status updates
    },

    // Status updates configuration
    statusUpdates: {
      numEvents: 3, // Trigger after 3 events
      timeInSeconds: 15, // Trigger every 15 seconds
      prompt: "Custom prompt for status updates", // Optional
    },
  });
  ```

  ### Configuration Options

  | Option             | Type   | Default       | Description                                                   |
  | ------------------ | ------ | ------------- | ------------------------------------------------------------- |
  | `numEvents`        | number | 10            | Trigger update after N events                                 |
  | `timeInSeconds`    | number | 30            | Trigger update every N seconds                                |
  | `model`            | string | agent default | Override model for summaries                                  |
  | `prompt`           | string | undefined     | Custom prompt appended to base system prompt (max 2000 chars) |
  | `statusComponents` | array  | undefined     | Structured status components (advanced)                       |

  ## Basic Example

  ```typescript
  const searchAgent = subAgent({
    id: "search-agent",
    name: "Search Agent",
    prompt: "Help users search and analyze information.",
    canUse: () => [searchTool],
  });

  export const searchDemo = agent({
    id: "search-demo",
    defaultSubAgent: searchAgent,
    subAgents: () => [searchAgent],

    models: {
      base: { model: "anthropic/claude-sonnet-4-5" },
      summarizer: { model: "anthropic/claude-sonnet-4-5" },
    },

    // Enable status updates
    statusUpdates: {
      numEvents: 2, // Update every 2 operations
      timeInSeconds: 10, // Or every 10 seconds
    },
  });
  ```

  **User Experience:**
  When a user asks "What's the weather in Paris?", they might see:

  1. "Searching for current weather information..."
  2. "Found weather data, analyzing conditions..."
  3. "Preparing detailed weather report..."

  ## Custom Prompts

  You can customize how status updates are generated by providing a custom prompt that gets appended to the base system prompt. Custom prompts are limited to 2000 characters to ensure optimal performance:

  ```typescript
  export const myAgent = agent({
    id: "my-agent",
    defaultSubAgent: myAgent,
    subAgents: () => [myAgent],
    models: {
      summarizer: { model: "anthropic/claude-sonnet-4-5" },
    },
    statusUpdates: {
      numEvents: 3,
      timeInSeconds: 15,
      // Custom prompt is appended to the base system prompt
      prompt: "KEEP ALL STATUS UPDATES SHORT AND CONCISE. Use bullet points and focus only on user-visible progress.",
    },
  });
  ```

  ### Custom Prompt Examples

  **Concise Updates:**

  ```typescript
  statusUpdates: {
    prompt: 'Keep responses to 5 words maximum. Use present tense verbs.',
  }
  // Results in: "Searching documentation", "Processing data", "Generating report"
  ```

  **Detailed Progress:**

  ```typescript
  statusUpdates: {
    prompt: 'Provide specific details about what was found or accomplished. Include relevant numbers or names when available.',
  }
  // Results in: "Found 12 API endpoints in authentication docs", "Retrieved user profile for john@example.com"
  ```

  **Branded Tone:**

  ```typescript
  statusUpdates: {
    prompt: 'Use a friendly, helpful tone. Start updates with action words like "Discovering", "Exploring", "Analyzing".',
  }
  // Results in: "Discovering relevant documentation...", "Analyzing search results for best matches"
  ```

  ## Default vs Structured Status Updates

  Status updates can be generated in two ways:

  ### Default Status Components

  When no `statusComponents` are configured, the system uses built-in default status components that generate structured labels with contextual details:

  * **`label`**: AI-generated user-friendly description (e.g., "Searching documentation for API examples")
  * **`details`**: Optional structured data with context about the operation

  The default status components are defined in [`packages/agents-sdk/src/default-status-schemas.ts`](https://github.com/inkeep/agents/blob/main/packages/agents-sdk/src/default-status-schemas.ts):

  ```typescript
  // Error loading local file from agents-run-api/src/utils/default-status-schemas.ts
  // ENOENT: no such file or directory, open '/vercel/path0/public/agents/agents-run-api/src/utils/default-status-schemas.ts'
  ```

  ### Custom Structured Components

  For complete control over status format, define custom structured status components using Zod schemas with the `statusComponent` helper:

  ```typescript
  import { z } from 'zod';
  import { statusComponent } from '@inkeep/agents-sdk';

  const toolSummary = statusComponent({
    type: 'tool_summary',
    description: 'Summary of tool calls and their purpose',
    detailsSchema: z.object({
      tool_name: z.string().describe('Name of tool used'),
      purpose: z.string().describe('Why this tool was called'),
      outcome: z.string().describe('What was discovered or accomplished'),
    }),
  });

  const progressUpdate = statusComponent({
    type: 'progress_update',
    description: 'Progress information with metrics',
    detailsSchema: z.object({
      current_step: z.string(),
      items_processed: z.number().optional(),
      status: z.enum(['in_progress', 'completed', 'pending']),
    }),
  });

  export const myAgent = agent({
    id: "my-agent",
    defaultSubAgent: myAgent,
    subAgents: () => [myAgent],
    models: {
      summarizer: { model: "anthropic/claude-sonnet-4-5" },
    },
    statusUpdates: {
      numEvents: 3,
      timeInSeconds: 15,
      statusComponents: [
        toolSummary.config,
        progressUpdate.config,
      ],
    },
  });
  ```

  <Callout type="info">
    The `statusComponent` helper provides type-safe status component definitions with automatic schema validation. Zod schemas are automatically converted to JSON Schema format at runtime.
  </Callout>

  ### Status Component API

  The `statusComponent` function accepts:

  | Property        | Type               | Required | Description                                         |
  | --------------- | ------------------ | -------- | --------------------------------------------------- |
  | `type`          | string             | Yes      | Unique identifier for this status component type    |
  | `description`   | string             | No       | Description of when/why this status is used         |
  | `detailsSchema` | `z.ZodObject<any>` | No       | Zod schema defining the structure of status details |

  **Note:** Use `.config` when passing status components to the agent configuration

  ## Frontend Integration

  Status updates arrive as `data-summary` events with AI-generated labels and contextual details:

  ```typescript
  // In your message processing
  message.parts.forEach((part) => {
    if (part.type === "data-summary") {
      const { type, label, details } = part.data;

      // Display the user-friendly status label
      displayStatusUpdate(label);

      // Handle optional structured details
      if (details) {
        // For default status components: contextual operation details
        // For custom components: your defined schema structure
        displayStructuredDetails(details);
      }

      // The 'type' field indicates the summary category
      console.log(`Status update type: ${type}`);
    }
  });
  ```
</SkillRule>

### Data Structure Examples

**Custom Status Components:**

```json
{
  "type": "tool_summary",
  "label": "Search completed successfully",
  "details": {
    "tool_name": "search_knowledge_base",
    "purpose": "Find API authentication documentation",
    "outcome": "Found 5 relevant articles with code examples"
  }
}
```

## Best Practices

### Timing Configuration

* **Quick Operations**: Use higher event counts (5-10) to avoid too many updates
* **Long Operations**: Use shorter time intervals (10-15 seconds) for responsiveness
* **Mixed Workloads**: Combine both triggers for balanced updates

### Model Selection

* **Faster Model**: Use a faster/cheaper model for status updates to reduce latency
* **Specialized Model**: Use a model optimized for summarization tasks

### User Experience

```typescript
// Good: Descriptive and actionable
"Searching through documentation to find API authentication methods...";
"Found OAuth2 setup prompt, now checking for code examples...";

// Avoid: Too technical or vague
"Agent thinking...";
"Processing data-operation with tool_invocation type...";
```

## Troubleshooting

### Updates Not Appearing

Check that:

1. Status updates are configured in your Agent
2. The summarizer model is specified
3. Events are being triggered (tool calls, generations)
4. The session hasn't ended prematurely

### Too Many/Few Updates

Adjust thresholds:

```typescript
statusUpdates: {
  numEvents: 5,        // Increase for fewer updates
  timeInSeconds: 20,   // Increase for less frequent updates
}
```

### Poor Update Quality

Improve with custom prompts:

```typescript
statusUpdates: {
  numEvents: 3,
  timeInSeconds: 15,
  prompt: `Generate a brief, user-friendly status update that explains what has been accomplished and what is currently happening. Focus on user-visible progress, not internal operations. Be specific about findings, not just actions.`
}
```

<Callout type="warning">
  Status updates are automatically paused during text streaming to avoid
  interrupting the response flow.
</Callout>

## Performance Considerations

* Status updates use additional model calls - consider costs for high-volume applications
* Updates are generated asynchronously and won't block main agent operations


# Function Tools in TypeScript SDK (/typescript-sdk/tools/function-tools)

Create custom JavaScript functions that your agents can execute in secure sandboxes



Function tools allow you to create custom JavaScript functions that agents can execute in secure, isolated sandboxes. Unlike [MCP servers](/typescript-sdk/tools/mcp-tools) that connect to external services, function tools run your own code directly within the agent framework.

<Tip>
  For sensitive function tools (write/delete actions), require explicit user approval before execution. See
  [Tool approvals](/typescript-sdk/tools/tool-approvals).
</Tip>

<SkillRule id="function-tools-overview" skills="typescript-sdk" title="Function Tools Overview" description="Use cases and how to create function tools">
  ## Overview

  Function tools are perfect for:

  * **Custom business logic** - Implement domain-specific calculations or workflows
  * **Data processing** - Transform, validate, or analyze data using JavaScript
  * **API integrations** - Make HTTP calls to services that don't have MCP servers
  * **Utility functions** - Create reusable helper functions for your agents
  * **Pipeline processing** - Chain tool outputs directly into subsequent tools without storing intermediate results

  ## Creating Function Tools

  ### Basic Function Tool

  ```typescript
  import { agent, functionTool } from "@inkeep/agents-sdk";

  const calculateBMI = functionTool({
    name: "calculate-bmi",
    description: "Calculate BMI and health category",
    inputSchema: {
      type: "object",
      properties: {
        weight: { type: "number", description: "Weight in kilograms" },
        height: { type: "number", description: "Height in meters" },
      },
      required: ["weight", "height"],
    },
    execute: async ({ weight, height }) => {
      const bmi = weight / (height * height);
      let category = "Normal";
      if (bmi < 18.5) category = "Underweight";
      else if (bmi >= 30) category = "Obese";

      return { bmi: Math.round(bmi * 10) / 10, category };
    },
  });

  const healthAgent = subAgent({
    id: "health-agent",
    name: "Health Assistant",
    prompt: "I help with health calculations using available tools.",
    canUse: () => [calculateBMI],
  });
  ```

  ### Function Tool with Dependencies

  Dependencies are automatically detected from your code and use the versions installed in your project:

  ```typescript
  const fetchJoke = functionTool({
    name: "fetch-joke",
    description: "Fetch a random programming joke",
    inputSchema: {
      type: "object",
      properties: {},
      required: [],
    },
    // Dependencies detected automatically from require() calls
    execute: async () => {
      const axios = require("axios"); // Uses version from your project
      const response = await axios.get(
        "https://official-joke-api.appspot.com/jokes/programming/random"
      );
      return {
        setup: response.data[0].setup,
        punchline: response.data[0].punchline,
      };
    },
  });
  ```

  ### Pinning Dependency Versions

  If you need to pin to specific versions, specify them explicitly:

  ```typescript
  const fetchJoke = functionTool({
    name: "fetch-joke",
    description: "Fetch a random programming joke",
    dependencies: {
      axios: "1.6.0", // Pin to exact version
    },
    execute: async () => {
      const axios = require("axios");
      // Uses axios 1.6.0 regardless of your project version
      return {
        joke: "Why do programmers prefer dark mode? Because light attracts bugs!",
      };
    },
  });
  ```

  ### Built-in Node.js Modules

  Function tools have access to Node.js built-in modules:

  ```typescript
  const generatePassword = functionTool({
    name: "generate-password",
    description: "Generate a secure random password",
    inputSchema: {
      type: "object",
      properties: {
        length: { type: "number", default: 12, description: "Password length" },
      },
    },
    execute: async ({ length = 12 }) => {
      const crypto = require("crypto");
      const charset =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";

      let password = "";
      for (let i = 0; i < length; i++) {
        password += charset[crypto.randomInt(0, charset.length)];
      }

      return { password, length };
    },
  });
  ```

  ## Input Schema Validation

  Function tools use JSON Schema to validate input parameters. This ensures your functions receive properly typed and validated data.

  ### Supported JSON Schema Types

  ```typescript
  // Example schema showing common types
  {
    type: "object",
    properties: {
      name: { type: "string", description: "User name" },
      age: { type: "number", minimum: 0, description: "User age" },
      active: { type: "boolean", description: "Is active" },
      tags: { type: "array", items: { type: "string" } },
      status: { type: "string", enum: ["pending", "approved"] }
    },
    required: ["name"]
  }
  ```
</SkillRule>

<SkillRule id="function-tools-execution" skills="typescript-sdk" title="Function Tools Execution Environment" description="Sandbox providers, runtime configuration, and security guarantees">
  ## Execution Environment

  Function tools run in secure, isolated sandboxes that provide a safe execution environment for your code.

  ### Sandbox Providers

  The framework supports multiple sandbox providers:

  * **Native** (default) - Uses Node.js child processes, works in most environments including cloud VMs, Docker, Kubernetes, and traditional hosting
  * **Vercel** - Uses Vercel Sandbox MicroVMs for serverless environments where child process spawning is restricted (Vercel, AWS Lambda, etc.)

  The sandbox provider is configured at the application level when deploying. See [Deploy to Vercel](/deployment/vercel#step-2-configure-sandbox-in-your-application) for serverless deployment configuration.

  ### Runtime Configuration

  Configure execution settings in your application:

  ```typescript
  import { createAgentsApp } from "@inkeep/agents-api";

  const app = createAgentsApp({
    sandboxConfig: {
      provider: "native", // or 'vercel' for serverless
      runtime: "node22", // Node.js 22 or 'typescript'
      timeout: 30000, // 30 second timeout
      vcpus: 2, // Max 2 concurrent executions
    },
  });
  ```

  ### Security & Isolation

  Function tools execute with strong security guarantees:

  * **Isolated execution** - Each function runs in its own sandbox
  * **No file system access** - Cannot read or write outside the sandbox
  * **Network restrictions** - Only through explicitly declared dependencies
  * **Resource limits** - CPU, memory, and execution time constraints enforced
  * **No state persistence** - Functions are stateless between executions

  ## Tool Output Pipelines

  Agents can pass the output of one tool directly as the input to another — no artifact creation required. This is useful when intermediate results are purely processing steps that don't need to be saved or shown to the user.

  For example, a pipeline that fetches a webpage and then processes its content:

  ```typescript
  const fetchPage = functionTool({
    name: "fetch-page",
    description: "Fetch the raw HTML content of a URL",
    inputSchema: {
      type: "object",
      properties: {
        url: { type: "string", description: "URL to fetch" },
      },
      required: ["url"],
    },
    execute: async ({ url }) => {
      const response = await fetch(url);
      return await response.text();
    },
  });

  const extractText = functionTool({
    name: "extract-text",
    description: "Extract readable text from HTML content",
    inputSchema: {
      type: "object",
      properties: {
        html: { type: "string", description: "HTML content to extract text from" },
      },
      required: ["html"],
    },
    execute: async ({ html }) => {
      // Strip tags and return plain text
      return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
    },
  });
  ```

  When the agent calls `fetch-page` and then `extract-text`, it passes the HTML output directly into the next call. The intermediate HTML never surfaces to the user. Design tools meant to be used in sequence so their return types match the input types of downstream tools.

  ### Best Practices

  * **Keep functions focused** - Each function should do one thing well
  * **Minimize dependencies** - Only include packages you actually need
  * **Handle errors gracefully** - Always catch and return meaningful errors
  * **Test independently** - Functions should be testable in isolation
  * **Document thoroughly** - Clear descriptions help agents use tools effectively
  * **Design for composability** - When tools form a pipeline, align return types with downstream input types
</SkillRule>


# Register MCP Servers as Tools (/typescript-sdk/tools/mcp-tools)

Learn how to add and configure MCP tools for your agents



<SkillRule id="mcp-tools-registration" skills="typescript-sdk" title="Registering MCP Servers as Tools" description="How to register and configure MCP tools for your agents">
  MCP tools connect to external MCP servers, enabling integration with a wide ecosystem of tools and services. Registering them as tools is a two step process:

  ### Step 1: Get an MCP server URL

  You need access to an MCP server before you can use its tools. Learn about connecting to Native servers, using Composio, using Gram, or building Custom servers in the [MCP Servers tutorial](/guides/mcp-servers/overview).

  ### Step 2: Register the MCP server as a tool in your agent

  ```typescript
  import { subAgent, mcpTool } from "@inkeep/agents-sdk";

  // Register an existing MCP server as a tool
  const knowledgeBaseTool = mcpTool({
    id: "knowledge-base-tool",
    name: "knowledge_base",
    description: "Search the company knowledge base for information",
    serverUrl: "https://kb.yourcompany.com/mcp", // URL of your deployed MCP server
  });

  const qaAgent = subAgent({
    id: "qa-agent",
    name: "QA Agent",
    description: "Responsible for answering questions about the company knowledge base.",
    prompt: `You are a Question-Answer agent that can answer questions about the company knowledge base. You have access to the knowledge base tool to search for information.`,
    canUse: () => [knowledgeBaseTool],
  });
  ```

  ## Custom Usage Instructions

  You can provide custom instructions for how agents should use tools from an MCP server by adding a `prompt` field:

  ```typescript
  import { mcpTool } from "@inkeep/agents-sdk";

  const weatherTool = mcpTool({
    id: "weather-tool",
    name: "weather_api",
    description: "Get current weather information",
    serverUrl: "https://weather.example.com/mcp",
    prompt: `When using weather tools, always ask for the user's location first if not provided. 
             Format temperature responses in both Celsius and Fahrenheit for better user experience.
             Include weather alerts if any severe conditions are detected.`,
  });
  ```

  The custom prompt is provided to agents as usage guidelines for all tools from this server, helping them understand the specific context and best practices for using these tools.

  ## Tool Output Pipelines

  MCP tool outputs can be passed directly as inputs to subsequent tools — including other MCP tools or function tools — without creating an artifact in between. Agents handle this automatically when tools are designed for sequential use.

  For example, a search MCP tool's results can be passed directly into a content extraction MCP tool, with the intermediate data never surfacing to the user. See [Function Tools — Tool Output Pipelines](/typescript-sdk/tools/function-tools#tool-output-pipelines) for more detail on designing pipeline-friendly tools.
</SkillRule>

<SkillRule id="mcp-tool-overrides" skills="typescript-sdk" title="MCP Tool Overrides" description="Customize how individual MCP tools appear to agents">
  ## Tool Overrides

  You can customize how individual tools from an MCP server appear to agents using tool overrides. This is useful for simplifying complex tools or providing better names and descriptions:

  ```typescript
  import { mcpTool } from "@inkeep/agents-sdk";
  import { z } from "zod";

  const apiTool = mcpTool({
    id: "complex-api-tool",
    name: "enterprise_api",
    description: "Enterprise API with many complex tools",
    serverUrl: "https://api.enterprise.com/mcp",
    toolOverrides: {
      // Override a specific tool by its original name
      "fetch_user_analytics_data_with_filters": {
        displayName: "get_user_stats", // Simpler name for agents
        description: "Get user statistics and analytics data", // Clearer description
        schema: z.object({
          userId: z.string().describe("User ID to fetch stats for"),
          timeframe: z.enum(["daily", "weekly", "monthly"]).describe("Time period for stats")
        })
      }
    }
  });
  ```

  ### Tool Override Options

  | Field         | Description                                                            |
  | ------------- | ---------------------------------------------------------------------- |
  | `displayName` | Custom name that agents will see (simpler than the original tool name) |
  | `description` | Custom description explaining what the tool does                       |
  | `schema`      | Simplified parameter schema with only the fields agents need           |

  <Note>
    Tool overrides are configured in the 

    [Visual Builder](/visual-builder/tools/mcp-servers#tool-overrides)

     for easier management, but can also be defined programmatically as shown above.
  </Note>

  <Note>
    Tip: When adding tools to an agent, you can also reference the tool in the agent's prompt to help the agent understand how to use the tool.
  </Note>
</SkillRule>

<SkillRule id="mcp-tool-auth" skills="typescript-sdk" title="MCP Tool Authentication" description="Authentication methods for MCP servers including API keys, OAuth, and custom headers">
  ## Authentication

  If your MCP server requires authentication, we support various authentication methods (via credentials):

  * **No Authentication** - For public APIs or internal services
  * **API Key** - For services requiring API keys
  * **Bearer Token** - For JWT or similar token-based authentication
  * **Token Authentication** - For custom token schemes
  * **OAuth Flows** - For standard OAuth 2.0 authentication
  * **OAuth 2.1 Flows** - For modern OAuth 2.1 "1-click" authentication

  For OAuth flows and OAuth 2.1 flows, it's recommended to use the [Visual Builder](/visual-builder/tools/mcp-servers) for easier configuration.

  See [Credentials](/typescript-sdk/credentials/overview) for detailed examples and implementation guidance for each authentication type.

  ### Custom Headers

  You can configure custom headers for your MCP server requests. Use credentials for sensitive information (API keys, tokens) and headers for non-sensitive metadata (user agent, version info, etc.).

  ```typescript
  import { mcpTool } from "@inkeep/agents-sdk";

  const customHeadersTool = mcpTool({
    id: "enterprise-api",
    name: "enterprise_data",
    description: "Enterprise API with custom headers",
    serverUrl: "https://enterprise.example.com/mcp",
    headers: {
      "User-Agent": "Inkeep-Agent/1.0",
      "X-Client-Version": "2024.1",
    },
  });
  ```
</SkillRule>

<SkillRule id="mcp-tool-selection" skills="typescript-sdk" title="Selecting MCP Tools" description="How to selectively enable tools from MCP servers">
  ## Selecting Tools

  <Tip>
    If some tools are sensitive (delete, write, payments, etc.), you can require explicit user approval before they run. See
    [Tool approvals](/typescript-sdk/tools/tool-approvals).
  </Tip>

  ### Selective Tool Activation

  Enable only specific tools from a server using MCP Server tool names in the `activeTools` field.

  ```typescript
  import { mcpTool } from "@inkeep/agents-sdk";

  const selectiveTool = mcpTool({
    id: "limited-server",
    name: "analytics_readonly",
    description: "Analytics server with limited access",
    serverUrl: "https://analytics.example.com/mcp",
    activeTools: ["get_metrics", "generate_report"], // Only these tools
  });
  ```

  ### Using `with` for Tool Selection

  While `activeTools` in `mcpTool` limits which tools are available from the server, you can further refine tool access at the sub agent level using `mcpTool.with`. This allows different agents to use different subsets of tools from the same MCP server.

  ```typescript
  import { agent, mcpTool } from "@inkeep/agents-sdk";

  // Define the MCP server with all available tools
  const customerSupportTool = mcpTool({
    id: "customer-support-tool",
    name: "Customer Support Tool",
    description: "Customer Support Tool",
    serverUrl: "https://customer-support.example.com/mcp",
  });

  // Agent that only uses specific tools from the server
  const customerSupportAgent = subAgent({
    id: "customer-support-agent",
    name: "Customer Support Agent",
    description: "Responsible for customer support",
    prompt: "You are a helpful assistant",
    canUse: () => [customerSupportTool.with({
        selectedTools: ["customer-support-tool"],
        headers: { "X-Custom-Header": "custom-value" }
      })],
  });
  ```
</SkillRule>

<SkillRule id="mcp-environment-aware" skills="typescript-sdk" title="Environment-Aware MCP Servers" description="Configure MCP tools to switch based on environment (dev vs production)">
  ## Environment-Aware MCP Servers

  You can also configure MCP tools to switch based on your environment. This is useful when you want to use different MCP tool configurations for development vs production.

  Creating environment-aware MCP tools is a two step process:

  ### Step 1: Define environment configurations

  <Tabs>
    <Tab title="Development Config">
      ```typescript title="projects/my-project/environments/development.env.ts"
      import { registerEnvironmentSettings, mcpTool } from '@inkeep/agents-sdk';
      import { CredentialStoreType } from '@inkeep/agents-core';

      export const development = registerEnvironmentSettings({
      mcpServers: {
      'customer_support_tool': mcpTool({
        id: 'customer-support-tool',
        name: 'Customer Support Tool',
        description: 'Customer Support Tool for development',
        serverUrl: 'http://localhost:3000/customer-support/mcp',
      })
      },
      });
      ```
    </Tab>

    <Tab title="Production Config">
      ```typescript title="projects/my-project/environments/production.env.ts"
      import { registerEnvironmentSettings, mcpTool } from '@inkeep/agents-sdk';
      import { CredentialStoreType } from '@inkeep/agents-core';

      export const production = registerEnvironmentSettings({
      mcpServers: {
      'customer_support_tool': mcpTool({
        id: 'customer-support-tool',
        name: 'Customer Support Tool',
        description: 'Customer Support Tool for production',
        serverUrl: 'https://customer-support.example.com/mcp',
      })
      },
      });
      ```
    </Tab>

    <Tab title="Index File">
      ```typescript title="projects/my-project/environments/index.ts"
      import { createEnvironmentSettings } from '@inkeep/agents-sdk';
      import { development } from './development.env';
      import { production } from './production.env';

      export const envSettings = createEnvironmentSettings({
      development,
      production,
      });
      ```
    </Tab>
  </Tabs>

  ### Step 2: Use the MCP tool in your sub agent

  ```typescript title="projects/my-project/agents/customer-support-agent.ts"
  import { subAgent } from "@inkeep/agents-sdk";
  import { envSettings } from "../environments";

  const customerSupportAgent = subAgent({
    id: "customer-support-agent",
    name: "Customer Support Agent",
    description: "Responsible for customer support",
    prompt: "You are a helpful assistant",
    canUse: () => [envSettings.getEnvironmentMcp('customer_support_tool')],
  });
  ```

  This pattern is useful if you want to keep track of different credentials for different environments. When you push your project using the [Inkeep CLI](/typescript-sdk/cli-reference#inkeep-push) `inkeep push` command with the `--env` flag, the credentials will be loaded from the appropriate environment file. For example, if you run `inkeep push --env development`, the credentials will be loaded from the `environments/development.env.ts` file.

  ### CLI Environment Variables

  The CLI respects these environment variables when using the `--env` flag:

  ```bash
  # Set environment name via environment variable
  export INKEEP_ENV=production
  inkeep push  # Uses production environment automatically

  # Override via CLI (takes precedence)
  inkeep push --env development  # Uses development instead
  ```
</SkillRule>


# Tool Approvals in TypeScript SDK (/typescript-sdk/tools/tool-approvals)

Require explicit user approval before running selected tools using the TypeScript SDK.



<SkillRule id="tool-approvals-config" skills="typescript-sdk" title="Tool Approvals Configuration" description="How to configure tools to require user approval before execution">
  ## Overview

  Tool approvals let you mark specific tools as “requires approval”. When the agent tries to run one, execution pauses and your client must approve or deny before the run can continue.

  ## Configure tools to require approval

  ### TypeScript SDK (MCP tools)

  ```typescript
  import { agent, tool } from "@inkeep/agents-sdk";

  const weatherAgent = agent("weather-forecast")
    .prompt("You help users get weather information.")
    .canUse(
      tool("weather-mcp").with({
        selectedTools: [
          "get_current_weather", // No approval required
          {
            name: "get_weather_forecast",
            needsApproval: true, // Requires user approval
          },
        ],
      })
    );
  ```

  See also: [MCP tools](/typescript-sdk/tools/mcp-tools)

  ### Visual Builder

  In the Visual Builder, select an MCP server node connected to a Sub Agent and toggle approval for individual tools in the Tool Configuration section. You can also bulk toggle approvals for all enabled tools using the header checkbox.

  See also: [Tool Approvals in Visual Builder](/visual-builder/tools/tool-approvals).

  ## Runtime Behavior

  When a tool requires approval, you’ll see a `tool-approval-request` in the stream.

  See [Chat API](/talk-to-your-agents/chat-api#tool-approval) for the tool event payloads (including approval requests, tool inputs, and tool outputs).

  ### Denial reason propagation

  When a user denies a tool request with a `reason`, the agent receives the denial reason as a clean, human-readable string. This allows the agent to understand why the request was denied and adapt its behavior — for example, trying a different approach based on the user's feedback.

  This is especially useful for redirecting agents. If a user denies a request for "San Francisco weather" with the reason "I want Tokyo instead", the agent can immediately act on that feedback and fetch Tokyo weather without requiring a separate follow-up message.
</SkillRule>

<SkillRule id="tool-approvals-response" skills="typescript-sdk" title="Responding to Tool Approval Requests" description="How to approve or deny tool execution requests">
  ## Responding to an approval request

  You can approve/deny via the chat endpoint (message part)

  `POST /run/api/chat` with an assistant message that includes a `tool-*` part:

  ```json
  {
    "conversationId": "conv_xyz789",
    "messages": [
      {
        "role": "assistant",
        "content": null,
        "parts": [
          {
            "type": "tool-get_weather_forecast",
            "toolCallId": "call_abc123def456",
            "state": "approval-responded",
            "approval": {
              "id": "aitxt-call_abc123def456",
              "approved": true,
              "reason": "User confirmed"
            }
          }
        ]
      }
    ]
  }
  ```

  ### Vercel AI SDK (`useChat`) example

  If you’re using the Vercel AI SDK UI message stream, approval requests show up as `tool-*` parts (e.g. `tool-getWeather`) with `state: "approval-requested"`.

  After calling `addToolApprovalResponse(...)`, call `sendMessage()` so the updated messages (including the approval response) are POSTed back to `/run/api/chat` and the run can continue.

  ```tsx
  "use client";

  import { useChat } from "@ai-sdk/react";

  export default function Chat() {
    const { messages, addToolApprovalResponse, sendMessage } = useChat();

    return (
      <>
        {messages.map((message) => (
          <div key={message.id}>
            {message.parts.map((part) => {
              if (part.type === "tool-getWeather") {
                switch (part.state) {
                  case "approval-requested":
                    return (
                      <div key={part.toolCallId}>
                        <p>Get weather for {part.input.city}?</p>
                        <button
                          type="button"
                          onClick={() => {
                            addToolApprovalResponse({
                              id: part.approval.id,
                              approved: true,
                            });
                            sendMessage();
                          }}
                        >
                          Approve
                        </button>
                        <button
                          type="button"
                          onClick={() => {
                            addToolApprovalResponse({
                              id: part.approval.id,
                              approved: false,
                            });
                            sendMessage();
                          }}
                        >
                          Deny
                        </button>
                      </div>
                    );
                  case "output-available":
                    return (
                      <div key={part.toolCallId}>
                        Weather in {part.input.city}: {String(part.output)}
                      </div>
                    );
                }
              }

              return null;
            })}
          </div>
        ))}
      </>
    );
  }
  ```

  <Note>
    Only `state: "approval-responded"` triggers an approval/denial. If your client
    includes other tool approval parts in message history (for example `state:
      "approval-requested"`), the server ignores them.
  </Note>

  ## Tool approvals in Slack

  When you run agents in Slack (via the Inkeep Slack app), tool approvals appear as interactive messages with **Approve** and **Deny** buttons.

  ### How it works

  1. The agent requests to run a tool that requires approval
  2. Slack posts a message showing the tool name and its input parameters
  3. The user clicks **Approve** or **Deny**
  4. The agent continues based on the user's decision

  ### Permissions

  Only the user who started the conversation can approve or deny a tool request. If another user clicks the buttons, they see an error message.

  ### Approval timeout

  If no one responds to an approval request within the timeout period, the request expires and the message updates to show "Expired". The agent receives a timeout notification and can inform the user.

  <Note>
    Tool approvals in Slack require account linking. Run `/inkeep link` to connect
    your Slack and Inkeep accounts before using agents with approval-required
    tools.
  </Note>

  ## Related docs

  * [Chat API](/talk-to-your-agents/chat-api)
  * [Data operations](/typescript-sdk/data-operations)
  * [Using the Slack App](/talk-to-your-agents/slack/commands)
  * [Custom Tool Approval UI](/talk-to-your-agents/interactive-components#custom-tool-approval-ui) — Replace the default approval card with your own React component using `IkpTool`
</SkillRule>


# Triggers Overview (/typescript-sdk/triggers/overview)

Create webhook endpoints that allow external services to invoke your agents



Triggers create webhook endpoints that allow external services (like GitHub, Slack, Stripe, or custom applications) to invoke your agents. When a webhook is received, the payload is validated, transformed, and used to start a new conversation with the agent.

## Message Format

When a trigger invokes an agent, it sends a **multi-part message** containing:

* **Text part** (optional): Generated from the `messageTemplate` using the payload data
* **Data part** (when payload exists): The transformed webhook payload as structured data

This allows agents to access both a human-readable summary via the text part and the full structured payload for detailed processing.

```json
{
  "parts": [
    { "kind": "text", "text": "New order received: #12345" },
    { "kind": "data", "data": { "orderId": "12345", "total": 99.99 }, "metadata": { "source": "trigger" } }
  ]
}
```

<Note>
  The data part is included whenever the webhook payload is not null/undefined. Even an empty object `{}` is included as a data part.
</Note>

<SkillRule id="triggers-overview" skills="typescript-sdk" title="Triggers Overview" description="Creating webhook endpoints to invoke agents from external services">
  ## Overview

  Triggers are useful for:

  * **Event-driven workflows** - Respond to events from external services (GitHub issues, Stripe payments, etc.)
  * **Third-party integrations** - Connect services that can send webhooks to your agents
  * **Automated tasks** - Trigger agent actions based on external events
  * **Custom applications** - Allow your own services to invoke agents via HTTP

  ## Creating Triggers

  ### Basic Trigger

  ```typescript
  import { agent, trigger, subAgent } from "@inkeep/agents-sdk";

  const slackTrigger = trigger({
    name: "Slack Messages",
    description: "Handle incoming Slack messages",
    messageTemplate: "New message from {{user.name}}: {{text}}",
  });

  export default agent({
    id: "support-agent",
    name: "Support Agent",
    defaultSubAgent: subAgent({
      id: "support",
      name: "Support Assistant",
      prompt: "You help answer support questions.",
    }),
    triggers: () => [slackTrigger],
  });
  ```

  ### Trigger with Structured Data Only

  If you don't need a human-readable text message, you can omit `messageTemplate` and the agent will receive only the structured payload data:

  ```typescript
  const dataTrigger = trigger({
    name: "Data Webhook",
    description: "Receives structured data for processing",
    // No messageTemplate - agent receives only the data part
    inputSchema: z.object({
      eventType: z.string(),
      payload: z.record(z.unknown()),
    }),
  });
  ```

  The agent will receive:

  ```json
  {
    "parts": [
      { "kind": "data", "data": { "eventType": "order.created", "payload": { ... } }, "metadata": { "source": "trigger" } }
    ]
  }
  ```

  ### Trigger with Input Validation

  Use JSON Schema or Zod to validate incoming webhook payloads:

  ```typescript
  import { z } from "zod";

  const githubWebhookTrigger = trigger({
    name: "GitHub Events",
    description: "Handle GitHub webhook events",
    inputSchema: z.object({
      action: z.string(),
      repository: z.object({
        full_name: z.string(),
      }),
      sender: z.object({
        login: z.string(),
      }),
    }),
    messageTemplate:
      "GitHub event: {{action}} on {{repository.full_name}} by {{sender.login}}",
    signingSecret: process.env.GITHUB_WEBHOOK_SECRET,
  });
  ```

  ### Trigger with Authentication

  Triggers support flexible header-based authentication. You can require any headers with expected values:

  <Tabs>
    <Tab title="Single Header">
      ```typescript
      const apiKeyTrigger = trigger({
        name: "Authenticated Webhook",
        messageTemplate: "Webhook received: {{message}}",
        authentication: {
          headers: [
            { name: "X-API-Key", value: process.env.WEBHOOK_API_KEY! },
          ],
        },
      });
      ```
    </Tab>

    <Tab title="Multiple Headers">
      ```typescript
      const multiAuthTrigger = trigger({
        name: "Multi-Header Auth Webhook",
        messageTemplate: "Webhook received: {{message}}",
        authentication: {
          headers: [
            { name: "X-API-Key", value: process.env.WEBHOOK_API_KEY! },
            { name: "X-Client-ID", value: process.env.WEBHOOK_CLIENT_ID! },
          ],
        },
      });
      ```
    </Tab>

    <Tab title="Authorization Header">
      ```typescript
      // For Bearer token or Basic auth, use the Authorization header
      const bearerTrigger = trigger({
        name: "Bearer Auth Webhook",
        messageTemplate: "Webhook received: {{message}}",
        authentication: {
          headers: [
            { 
              name: "Authorization", 
              value: `Bearer ${process.env.WEBHOOK_BEARER_TOKEN!}` 
            },
          ],
        },
      });
      ```
    </Tab>
  </Tabs>

  <Note>
    Header values are securely hashed before storage. The original values are never stored in the database.
  </Note>
</SkillRule>

<SkillRule id="triggers-config-options" skills="typescript-sdk" title="Trigger Configuration Options" description="Configuration options for triggers including authentication, schemas, and transforms">
  ## Configuration Options

  | Option            | Type                  | Required | Description                                                          |
  | ----------------- | --------------------- | -------- | -------------------------------------------------------------------- |
  | `name`            | `string`              | Yes      | Human-readable name for the trigger                                  |
  | `description`     | `string`              | No       | Description of what the trigger does                                 |
  | `enabled`         | `boolean`             | No       | Whether the trigger is active (default: `true`)                      |
  | `inputSchema`     | `object \| ZodObject` | No       | JSON Schema or Zod schema for payload validation                     |
  | `messageTemplate` | `string`              | No       | Optional template for the text part of the message sent to the agent |
  | `authentication`  | `object`              | No       | Header-based authentication configuration                            |
  | `signingSecret`   | `string`              | No       | HMAC-SHA256 secret for signature verification                        |
  | `outputTransform` | `object`              | No       | Payload transformation configuration                                 |

  ### Authentication Configuration

  ```typescript
  authentication: {
    headers: [
      { name: "Header-Name", value: "expected-value" },
      // Add more headers as needed
    ],
  }
  ```

  Each incoming request must include all configured headers with matching values. If any header is missing or has an incorrect value, the request is rejected with a `401` or `403` status.

  ### Message Templates

  Message templates are **optional** and use `{{placeholder}}` syntax to interpolate values from the webhook payload. When provided, the interpolated text is included as a text part in the message alongside the structured data part:

  ```typescript
  const trigger = trigger({
    name: "Order Webhook",
    messageTemplate: `
  New order received:
  - Order ID: {{order.id}}
  - Customer: {{customer.name}} ({{customer.email}})
  - Total: ${{order.total}}
  - Items: {{order.items.length}} items

  Please process this order and send a confirmation.
    `.trim(),
  });
  ```

  Nested properties are accessed with dot notation (`{{customer.email}}`). Array length can be accessed with `.length`.

  ### Payload Transformation

  Transform incoming payloads before they reach the message template using `outputTransform`. You can choose between two approaches:

  | Approach                  | Best For                                           | Complexity |
  | ------------------------- | -------------------------------------------------- | ---------- |
  | **Object Transformation** | Simple field remapping                             | Low        |
  | **JMESPath**              | Complex filtering, restructuring, array operations | High       |

  <Note>
    These options are **mutually exclusive**. Use either `jmespath` OR `objectTransformation`, not both. If both are provided, `jmespath` takes priority.
  </Note>

  #### Object Transformation (Recommended for Simple Cases)

  Use `objectTransformation` when you just need to remap fields from the payload. Each key becomes a field in the transformed output, and each value is a JMESPath path to extract from the input:

  ```typescript
  const transformingTrigger = trigger({
    name: "Transformed Webhook",
    messageTemplate: "Event: {{eventType}} - {{summary}}",
    outputTransform: {
      // JMESPath expression for complex transformations
      jmespath: "{ eventType: type, summary: data.description }",
    },
  });

  // Or use simple object mapping
  const mappingTrigger = trigger({
    name: "Mapped Webhook",
    messageTemplate: "User {{userName}} performed {{actionName}}",
    outputTransform: {
      objectTransformation: {
        userName: "payload.user.display_name",
        actionName: "payload.action.type",
      },
    },
  });

  // Input: { payload: { user: { display_name: "Alice" }, action: { type: "click" } } }
  // Transformed: { userName: "Alice", actionName: "click" }
  // Message: "User Alice performed click"
  ```

  #### JMESPath (For Complex Transformations)

  Use `jmespath` when you need more powerful transformations like filtering arrays, conditional logic, or complex restructuring. [JMESPath](https://jmespath.org/) is a query language for JSON:

  ```typescript
  const transformingTrigger = trigger({
    name: "Transformed Webhook",
    messageTemplate: "Event: {{eventType}} - {{summary}}",
    outputTransform: {
      jmespath: "{ eventType: type, summary: data.description }",
    },
    authentication: { type: "none" },
  });

  // Input: { type: "alert", data: { description: "Server down" } }
  // Transformed: { eventType: "alert", summary: "Server down" }
  // Message: "Event: alert - Server down"
  ```

  **Advanced JMESPath example** - filtering and projecting arrays:

  ```typescript
  const advancedTrigger = trigger({
    name: "Active Users Alert",
    messageTemplate: "Active users: {{activeUserNames}}",
    outputTransform: {
      jmespath: "{ activeUserNames: join(', ', users[?active==`true`].name) }",
    },
    authentication: { type: "none" },
  });

  // Input: { users: [{ name: "Alice", active: true }, { name: "Bob", active: false }] }
  // Transformed: { activeUserNames: "Alice" }
  // Message: "Active users: Alice"
  ```

  <Tip>
    Object Transformation is converted to JMESPath under the hood. For example, `{ userName: "user.name" }` becomes the JMESPath expression `{ userName: user.name }`.
  </Tip>

  ### Signature Verification

  <Warning>
    **Beta Feature**: Signature verification is currently in beta. The API may change in future releases.
  </Warning>

  For services that sign webhooks (like GitHub, Stripe, Slack), use `signingSecret`:

  ```typescript
  const githubTrigger = trigger({
    name: "GitHub Webhook",
    messageTemplate: "GitHub: {{action}} on {{repository.full_name}}",
    signingSecret: process.env.GITHUB_WEBHOOK_SECRET,
  });
  ```

  The framework automatically verifies HMAC-SHA256 signatures in the `X-Signature-256` header.

  <Tip>
    For services like GitHub that sign webhooks, you often don't need header authentication—the signature verification provides security. Use `signingSecret` alone for these cases.
  </Tip>

  ## Webhook URL

  After pushing your agent configuration, each trigger gets a unique webhook URL:

  ```
  POST /tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{triggerId}
  ```

  You can find the full webhook URL in the Inkeep dashboard or via the API response when creating/listing triggers.

  ## Invocation Tracking

  Every webhook invocation is tracked with:

  * **Invocation ID** - Unique identifier for the invocation
  * **Status** - `pending`, `success`, or `failed`
  * **Request Payload** - Original webhook payload
  * **Transformed Payload** - Payload after transformation
  * **Conversation ID** - ID of the conversation created
  * **Error Message** - Error details if the invocation failed

  Query invocation history via the API:

  ```bash
  # List invocations for a trigger
  curl "https://api.inkeep.com/v1/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{triggerId}/invocations"

  # Filter by status or date range
  curl "https://api.inkeep.com/v1/.../invocations?status=failed&from=2024-01-01T00:00:00Z"
  ```

  ## Complete Example

  Here's a complete example with a GitHub webhook trigger that handles issue events:

  ```typescript
  import { z } from "zod";
  import { agent, trigger, subAgent } from "@inkeep/agents-sdk";

  // Define the expected GitHub webhook payload
  const githubIssueSchema = z.object({
    action: z.enum(["opened", "closed", "reopened", "edited"]),
    issue: z.object({
      number: z.number(),
      title: z.string(),
      body: z.string().nullable(),
      user: z.object({
        login: z.string(),
      }),
      labels: z.array(
        z.object({
          name: z.string(),
        })
      ),
    }),
    repository: z.object({
      full_name: z.string(),
    }),
  });

  const githubIssueTrigger = trigger({
    name: "GitHub Issues",
    description: "Responds to GitHub issue events",
    enabled: true,
    inputSchema: githubIssueSchema,
    messageTemplate: `
  GitHub Issue {{action}}:
  - Repository: {{repository.full_name}}
  - Issue #{{issue.number}}: {{issue.title}}
  - Author: {{issue.user.login}}
  - Body: {{issue.body}}

  Please analyze this issue and provide a helpful response.
    `.trim(),
    signingSecret: process.env.GITHUB_WEBHOOK_SECRET,
  });

  const issueResponder = subAgent({
    id: "issue-responder",
    name: "Issue Responder",
    prompt: `You are a helpful assistant that responds to GitHub issues.
  When an issue is opened, analyze the content and provide:
  1. A summary of the issue
  2. Suggested labels if appropriate
  3. Initial guidance or questions for the issue author`,
  });

  export default agent({
    id: "github-bot",
    name: "GitHub Bot",
    defaultSubAgent: issueResponder,
    triggers: () => [githubIssueTrigger],
  });
  ```
</SkillRule>

## Best Practices

1. **Always validate input** - Use `inputSchema` to ensure payloads match expected format
2. **Use signing secrets** - Verify webhook authenticity when the source supports it
3. **Use header auth for custom webhooks** - When the source doesn't sign webhooks, use header-based authentication
4. **Write clear templates** - Make message templates readable and include relevant context
5. **Handle errors gracefully** - Check invocation status and logs for failed webhooks
6. **Use descriptive names** - Name triggers clearly to identify their purpose in dashboards
7. **Store secrets in environment variables** - Never hardcode authentication values


# Scheduled Triggers in TypeScript SDK (/typescript-sdk/triggers/scheduled)

Define scheduled triggers in the TypeScript SDK to run agents on a cron schedule or one-time execution



Scheduled triggers run agents on a time-based schedule using cron expressions or at a one-time future timestamp. See [Scheduled Triggers](/visual-builder/triggers/scheduled) for the dashboard configuration walkthrough.

## Creating Scheduled Triggers

Use the `scheduledTrigger()` factory function to create both recurring and one-time scheduled triggers. The schema validation enforces that you specify either `cronExpression` or `runAt`, but not both.

```typescript
import { scheduledTrigger } from "@inkeep/agents-sdk";

// Recurring schedule with cron
const dailyReport = scheduledTrigger({
  name: "Daily Report",
  cronExpression: "0 9 * * MON-FRI",
  cronTimezone: "America/New_York",
  messageTemplate: "Generate the daily status report",
  payload: { reportType: "daily" },
  maxRetries: 3,
  retryDelaySeconds: 60,
  timeoutSeconds: 300,
});

// One-time schedule
const oneTimeTask = scheduledTrigger({
  name: "Migration Task",
  runAt: "2025-04-01T00:00:00Z",
  messageTemplate: "Run the migration",
  maxRetries: 5,
  retryDelaySeconds: 300,
  timeoutSeconds: 780,
});
```

## Configuration Options

| Option              | Type      | Required          | Default | Description                                                               |
| ------------------- | --------- | ----------------- | ------- | ------------------------------------------------------------------------- |
| `name`              | `string`  | Yes               | —       | Human-readable name                                                       |
| `description`       | `string`  | No                | —       | Description of the trigger's purpose                                      |
| `enabled`           | `boolean` | No                | `true`  | Whether the trigger is active                                             |
| `cronExpression`    | `string`  | One of cron/runAt | —       | 5-field cron expression (`minute hour day month weekday`)                 |
| `cronTimezone`      | `string`  | No                | `UTC`   | IANA timezone for cron evaluation                                         |
| `runAt`             | `string`  | One of cron/runAt | —       | ISO 8601 timestamp for one-time execution                                 |
| `messageTemplate`   | `string`  | No                | —       | Template with `{{placeholder}}` syntax                                    |
| `payload`           | `object`  | No                | —       | Static JSON payload passed to the agent                                   |
| `maxRetries`        | `number`  | No                | `1`     | Max retry attempts on failure (0–10)                                      |
| `retryDelaySeconds` | `number`  | No                | `60`    | Seconds between retries (10–3600)                                         |
| `timeoutSeconds`    | `number`  | No                | `780`   | Execution timeout in seconds (30–780)                                     |
| `runAsUserId`       | `string`  | No                | —       | User ID whose identity and credentials are used during execution          |
| `createdBy`         | `string`  | —                 | —       | Read-only. User ID of the trigger creator, set automatically on creation. |

<Note>
  You must specify either `cronExpression` or `runAt`, but not both. The system validates this at creation time.
</Note>

## Overriding Configuration with `.with()`

Use the `.with()` method to create a variant of an existing scheduled trigger:

```typescript
const dailyReport = scheduledTrigger({
  name: "Daily Report",
  cronExpression: "0 9 * * *",
  messageTemplate: "Generate the daily report",
});

const disabledReport = dailyReport.with({ enabled: false });
const weekdayReport = dailyReport.with({
  name: "Weekday Report",
  cronExpression: "0 9 * * MON-FRI",
});
```

## User-Scoped Execution

Set `runAsUserId` to execute the trigger with a specific user's identity, enabling access to their connected credentials (e.g., per-user OAuth tokens for GitHub, Slack, or Jira):

```typescript
import { scheduledTrigger } from "@inkeep/agents-sdk";

const userScopedReport = scheduledTrigger({
  name: "My Daily Report",
  cronExpression: "0 9 * * MON-FRI",
  messageTemplate: "Generate my daily status report",
  runAsUserId: "user_abc123",
});
```

When `runAsUserId` is set, agents automatically use that user's profile timezone for time-aware responses. Note that `cronTimezone` is still used for schedule calculation.

See [Scheduled Triggers](/visual-builder/triggers/scheduled) for details on permissions, admin delegation, and runtime behavior.

## Complete Example

```typescript
import { agent, subAgent, scheduledTrigger } from "@inkeep/agents-sdk";

// Recurring schedule with cron
const dailySummary = scheduledTrigger({
  name: "Daily Summary",
  cronExpression: "0 9 * * MON-FRI",
  cronTimezone: "America/New_York",
  messageTemplate:
    "Generate a summary of yesterday's activity. Report type: {{reportType}}",
  payload: { reportType: "daily" },
  maxRetries: 3,
  retryDelaySeconds: 120,
  timeoutSeconds: 600,
});

const hourlyHealthCheck = scheduledTrigger({
  name: "Hourly Health Check",
  cronExpression: "0 * * * *",
  messageTemplate: "Run a health check on all connected services",
  maxRetries: 2,
  timeoutSeconds: 120,
});

// One-time schedule
const dataMigration = scheduledTrigger({
  name: "Q1 Data Migration",
  runAt: "2025-04-01T00:00:00Z",
  messageTemplate: "Migrate Q1 data to the new schema",
  maxRetries: 5,
  retryDelaySeconds: 300,
  timeoutSeconds: 780,
});

const reportGenerator = subAgent({
  id: "report-generator",
  name: "Report Generator",
  prompt: "You generate reports and summaries based on system activity.",
});

export default agent({
  id: "automation-agent",
  name: "Automation Agent",
  defaultSubAgent: reportGenerator,
  scheduledTriggers: () => [
    dailySummary,
    hourlyHealthCheck,
    dataMigration,
  ],
});
```

<Note>
  Scheduled triggers are managed separately from webhook triggers via dedicated API endpoints. They are scoped to an agent and can be created, updated, enabled/disabled, and deleted through the API or Manage UI.
</Note>

## Best Practices

1. **Set appropriate timeouts** - Ensure `timeoutSeconds` is long enough for the agent to complete
2. **Configure retries for critical tasks** - Use `maxRetries` for important scheduled jobs
3. **Monitor invocations** - Check the invocation history for failed runs
4. **Use one-time triggers for deferred work** - Prefer `runAt` over cron for tasks that should execute exactly once


# Webhook Triggers in TypeScript SDK (/typescript-sdk/triggers/webhooks)

Create webhook endpoints in the TypeScript SDK that allow external services to invoke your agents



Triggers create webhook endpoints that allow external services (like GitHub, Slack, Stripe, or custom applications) to invoke your agents. When a webhook is received, the payload is validated, transformed, and used to start a new conversation with the agent.

## Message Format

When a trigger invokes an agent, it sends a **multi-part message** containing:

* **Text part** (optional): Generated from the `messageTemplate` using the payload data
* **Data part** (when payload exists): The transformed webhook payload as structured data

This allows agents to access both a human-readable summary via the text part and the full structured payload for detailed processing.

```json
{
  "parts": [
    { "kind": "text", "text": "New order received: #12345" },
    { "kind": "data", "data": { "orderId": "12345", "total": 99.99 }, "metadata": { "source": "trigger" } }
  ]
}
```

<Note>
  The data part is included whenever the webhook payload is not null/undefined. Even an empty object `{}` is included as a data part.
</Note>

<SkillRule id="triggers-overview" skills="typescript-sdk" title="Triggers Overview" description="Creating webhook endpoints to invoke agents from external services">
  ## Overview

  Triggers are useful for:

  * **Event-driven workflows** - Respond to events from external services (GitHub issues, Stripe payments, etc.)
  * **Third-party integrations** - Connect services that can send webhooks to your agents
  * **Automated tasks** - Trigger agent actions based on external events
  * **Custom applications** - Allow your own services to invoke agents via HTTP

  <Warning>
    \*\*User-scoped MCP servers require a configured `runAsUserId`. Without `runAsUserId` set on the trigger, tools that require user-scoped credentials will fail. See [User vs Project MCPs](/visual-builder/tools/user-vs-project-mcp) for more details.
  </Warning>

  ## Creating Triggers

  ### Basic Trigger

  ```typescript
  import { agent, trigger, subAgent } from "@inkeep/agents-sdk";

  const slackTrigger = trigger({
    name: "Slack Messages",
    description: "Handle incoming Slack messages",
    messageTemplate: "New message from {{user.name}}: {{text}}",
  });

  export default agent({
    id: "support-agent",
    name: "Support Agent",
    defaultSubAgent: subAgent({
      id: "support",
      name: "Support Assistant",
      prompt: "You help answer support questions.",
    }),
    triggers: () => [slackTrigger],
  });
  ```

  ### Trigger with Structured Data Only

  If you don't need a human-readable text message, you can omit `messageTemplate` and the agent will receive only the structured payload data:

  ```typescript
  const dataTrigger = trigger({
    name: "Data Webhook",
    description: "Receives structured data for processing",
    // No messageTemplate - agent receives only the data part
    inputSchema: z.object({
      eventType: z.string(),
      payload: z.record(z.unknown()),
    }),
  });
  ```

  The agent will receive:

  ```json
  {
    "parts": [
      { "kind": "data", "data": { "eventType": "order.created", "payload": { ... } }, "metadata": { "source": "trigger" } }
    ]
  }
  ```

  ### Trigger with Input Validation

  Use JSON Schema or Zod to validate incoming webhook payloads:

  ```typescript
  import { z } from "zod";

  const githubWebhookTrigger = trigger({
    name: "GitHub Events",
    description: "Handle GitHub webhook events",
    inputSchema: z.object({
      action: z.string(),
      repository: z.object({
        full_name: z.string(),
      }),
      sender: z.object({
        login: z.string(),
      }),
    }),
    messageTemplate:
      "GitHub event: {{action}} on {{repository.full_name}} by {{sender.login}}",
    signingSecret: process.env.GITHUB_WEBHOOK_SECRET,
  });
  ```

  ### Trigger with Authentication

  Triggers support flexible header-based authentication. You can require any headers with expected values:

  <Tabs>
    <Tab title="Single Header">
      ```typescript
      const apiKeyTrigger = trigger({
        name: "Authenticated Webhook",
        messageTemplate: "Webhook received: {{message}}",
        authentication: {
          headers: [
            { name: "X-API-Key", value: process.env.WEBHOOK_API_KEY! },
          ],
        },
      });
      ```
    </Tab>

    <Tab title="Multiple Headers">
      ```typescript
      const multiAuthTrigger = trigger({
        name: "Multi-Header Auth Webhook",
        messageTemplate: "Webhook received: {{message}}",
        authentication: {
          headers: [
            { name: "X-API-Key", value: process.env.WEBHOOK_API_KEY! },
            { name: "X-Client-ID", value: process.env.WEBHOOK_CLIENT_ID! },
          ],
        },
      });
      ```
    </Tab>

    <Tab title="Authorization Header">
      ```typescript
      // For Bearer token or Basic auth, use the Authorization header
      const bearerTrigger = trigger({
        name: "Bearer Auth Webhook",
        messageTemplate: "Webhook received: {{message}}",
        authentication: {
          headers: [
            { 
              name: "Authorization", 
              value: `Bearer ${process.env.WEBHOOK_BEARER_TOKEN!}` 
            },
          ],
        },
      });
      ```
    </Tab>
  </Tabs>

  <Note>
    Header values are securely hashed before storage. The original values are never stored in the database.
  </Note>
</SkillRule>

<SkillRule id="triggers-config-options" skills="typescript-sdk" title="Trigger Configuration Options" description="Configuration options for triggers including authentication, schemas, and transforms">
  ## Configuration Options

  | Option            | Type                  | Required | Description                                                          |
  | ----------------- | --------------------- | -------- | -------------------------------------------------------------------- |
  | `name`            | `string`              | Yes      | Human-readable name for the trigger                                  |
  | `description`     | `string`              | No       | Description of what the trigger does                                 |
  | `enabled`         | `boolean`             | No       | Whether the trigger is active (default: `true`)                      |
  | `inputSchema`     | `object \| ZodObject` | No       | JSON Schema or Zod schema for payload validation                     |
  | `messageTemplate` | `string`              | No       | Optional template for the text part of the message sent to the agent |
  | `authentication`  | `object`              | No       | Header-based authentication configuration                            |
  | `signingSecret`   | `string`              | No       | HMAC-SHA256 secret for signature verification                        |
  | `outputTransform` | `object`              | No       | Payload transformation configuration                                 |
  | `runAsUserId`     | `string`              | No       | User ID whose identity and credentials are used during execution     |

  ### Authentication Configuration

  ```typescript
  authentication: {
    headers: [
      { name: "Header-Name", value: "expected-value" },
      // Add more headers as needed
    ],
  }
  ```

  Each incoming request must include all configured headers with matching values. If any header is missing or has an incorrect value, the request is rejected with a `401` or `403` status.

  ### Message Templates

  Message templates are **optional** and use `{{placeholder}}` syntax to interpolate values from the webhook payload. When provided, the interpolated text is included as a text part in the message alongside the structured data part:

  ```typescript
  const trigger = trigger({
    name: "Order Webhook",
    messageTemplate: `
  New order received:
  - Order ID: {{order.id}}
  - Customer: {{customer.name}} ({{customer.email}})
  - Total: ${{order.total}}
  - Items: {{order.items.length}} items

  Please process this order and send a confirmation.
    `.trim(),
  });
  ```

  Nested properties are accessed with dot notation (`{{customer.email}}`). Array length can be accessed with `.length`.

  ### Payload Transformation

  Transform incoming payloads before they reach the message template using `outputTransform`. You can choose between two approaches:

  | Approach                  | Best For                                           | Complexity |
  | ------------------------- | -------------------------------------------------- | ---------- |
  | **Object Transformation** | Simple field remapping                             | Low        |
  | **JMESPath**              | Complex filtering, restructuring, array operations | High       |

  <Note>
    These options are **mutually exclusive**. Use either `jmespath` OR `objectTransformation`, not both. If both are provided, `jmespath` takes priority.
  </Note>

  #### Object Transformation (Recommended for Simple Cases)

  Use `objectTransformation` when you just need to remap fields from the payload. Each key becomes a field in the transformed output, and each value is a JMESPath path to extract from the input:

  ```typescript
  const transformingTrigger = trigger({
    name: "Transformed Webhook",
    messageTemplate: "Event: {{eventType}} - {{summary}}",
    outputTransform: {
      // JMESPath expression for complex transformations
      jmespath: "{ eventType: type, summary: data.description }",
    },
  });

  // Or use simple object mapping
  const mappingTrigger = trigger({
    name: "Mapped Webhook",
    messageTemplate: "User {{userName}} performed {{actionName}}",
    outputTransform: {
      objectTransformation: {
        userName: "payload.user.display_name",
        actionName: "payload.action.type",
      },
    },
  });

  // Input: { payload: { user: { display_name: "Alice" }, action: { type: "click" } } }
  // Transformed: { userName: "Alice", actionName: "click" }
  // Message: "User Alice performed click"
  ```

  #### JMESPath (For Complex Transformations)

  Use `jmespath` when you need more powerful transformations like filtering arrays, conditional logic, or complex restructuring. [JMESPath](https://jmespath.org/) is a query language for JSON:

  ```typescript
  const transformingTrigger = trigger({
    name: "Transformed Webhook",
    messageTemplate: "Event: {{eventType}} - {{summary}}",
    outputTransform: {
      jmespath: "{ eventType: type, summary: data.description }",
    },
    authentication: { type: "none" },
  });

  // Input: { type: "alert", data: { description: "Server down" } }
  // Transformed: { eventType: "alert", summary: "Server down" }
  // Message: "Event: alert - Server down"
  ```

  **Advanced JMESPath example** - filtering and projecting arrays:

  ```typescript
  const advancedTrigger = trigger({
    name: "Active Users Alert",
    messageTemplate: "Active users: {{activeUserNames}}",
    outputTransform: {
      jmespath: "{ activeUserNames: join(', ', users[?active==`true`].name) }",
    },
    authentication: { type: "none" },
  });

  // Input: { users: [{ name: "Alice", active: true }, { name: "Bob", active: false }] }
  // Transformed: { activeUserNames: "Alice" }
  // Message: "Active users: Alice"
  ```

  <Tip>
    Object Transformation is converted to JMESPath under the hood. For example, `{ userName: "user.name" }` becomes the JMESPath expression `{ userName: user.name }`.
  </Tip>

  ### Signature Verification

  <Warning>
    **Beta Feature**: Signature verification is currently in beta. The API may change in future releases.
  </Warning>

  For services that sign webhooks (like GitHub, Stripe, Slack), use `signingSecret`:

  ```typescript
  const githubTrigger = trigger({
    name: "GitHub Webhook",
    messageTemplate: "GitHub: {{action}} on {{repository.full_name}}",
    signingSecret: process.env.GITHUB_WEBHOOK_SECRET,
  });
  ```

  The framework automatically verifies HMAC-SHA256 signatures in the `X-Signature-256` header.

  <Tip>
    For services like GitHub that sign webhooks, you often don't need header authentication—the signature verification provides security. Use `signingSecret` alone for these cases.
  </Tip>

  ## User-Scoped Execution

  By default, webhook triggers execute without a user identity — tools with user-scoped credentials will fail. Setting `runAsUserId` makes the trigger execute as a specific user, enabling access to their connected credentials (e.g., per-user OAuth tokens for GitHub, Slack, or Jira).

  ```typescript
  trigger({
    id: "github-pr-review",
    name: "GitHub PR Review",
    messageTemplate: "New PR opened: {{title}}",
    runAsUserId: "user_abc123",
  });
  ```

  ### Permission Rules

  * Any user with `edit` permission can set `runAsUserId` to themselves.
  * Org admins and owners can set `runAsUserId` to any user in the organization.

  ## Webhook URL

  After pushing your agent configuration, each trigger gets a unique webhook URL:

  ```
  POST /tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{triggerId}
  ```

  You can find the full webhook URL in the Inkeep dashboard or via the API response when creating/listing triggers.

  ## Invocation Tracking

  Every webhook invocation is tracked with:

  * **Invocation ID** - Unique identifier for the invocation
  * **Status** - `pending`, `success`, or `failed`
  * **Request Payload** - Original webhook payload
  * **Transformed Payload** - Payload after transformation
  * **Conversation ID** - ID of the conversation created
  * **Error Message** - Error details if the invocation failed

  Query invocation history via the API:

  ```bash
  # List invocations for a trigger
  curl "https://api.inkeep.com/v1/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/triggers/{triggerId}/invocations"

  # Filter by status or date range
  curl "https://api.inkeep.com/v1/.../invocations?status=failed&from=2024-01-01T00:00:00Z"
  ```

  ## Complete Example

  Here's a complete example with a GitHub webhook trigger that handles issue events:

  ```typescript
  import { z } from "zod";
  import { agent, trigger, subAgent } from "@inkeep/agents-sdk";

  // Define the expected GitHub webhook payload
  const githubIssueSchema = z.object({
    action: z.enum(["opened", "closed", "reopened", "edited"]),
    issue: z.object({
      number: z.number(),
      title: z.string(),
      body: z.string().nullable(),
      user: z.object({
        login: z.string(),
      }),
      labels: z.array(
        z.object({
          name: z.string(),
        })
      ),
    }),
    repository: z.object({
      full_name: z.string(),
    }),
  });

  const githubIssueTrigger = trigger({
    name: "GitHub Issues",
    description: "Responds to GitHub issue events",
    enabled: true,
    inputSchema: githubIssueSchema,
    messageTemplate: `
  GitHub Issue {{action}}:
  - Repository: {{repository.full_name}}
  - Issue #{{issue.number}}: {{issue.title}}
  - Author: {{issue.user.login}}
  - Body: {{issue.body}}

  Please analyze this issue and provide a helpful response.
    `.trim(),
    signingSecret: process.env.GITHUB_WEBHOOK_SECRET,
  });

  const issueResponder = subAgent({
    id: "issue-responder",
    name: "Issue Responder",
    prompt: `You are a helpful assistant that responds to GitHub issues.
  When an issue is opened, analyze the content and provide:
  1. A summary of the issue
  2. Suggested labels if appropriate
  3. Initial guidance or questions for the issue author`,
  });

  export default agent({
    id: "github-bot",
    name: "GitHub Bot",
    defaultSubAgent: issueResponder,
    triggers: () => [githubIssueTrigger],
  });
  ```
</SkillRule>

## Best Practices

1. **Always validate input** - Use `inputSchema` to ensure payloads match expected format
2. **Use signing secrets** - Verify webhook authenticity when the source supports it
3. **Use header auth for custom webhooks** - When the source doesn't sign webhooks, use header-based authentication
4. **Write clear templates** - Make message templates readable and include relevant context
5. **Handle errors gracefully** - Check invocation status and logs for failed webhooks
6. **Use descriptive names** - Name triggers clearly to identify their purpose in dashboards
7. **Store secrets in environment variables** - Never hardcode authentication values


# Artifact Components in Visual Builder (/visual-builder/structured-outputs/artifact-components)

Configure artifact components in the Visual Builder to store citations and structured data from tool results.



<>
  Artifacts are structured components that capture and store source information when agents interact with tools or other agents. They act as a record-keeping system, automatically documenting where information comes from during tool and agent interactions.

  ## How Artifacts Work

  When an agent uses a tool (whether that's another agent or a utility tool), the response is automatically parsed to create artifacts. These artifacts store:

  * The source of the information (tool/agent used)
  * The relevant content from the response
  * Metadata about the interaction

  ## Oversized Artifacts

  When tool results exceed **30% of the model's context window**, they're marked as **oversized** to prevent context overflow.

  **What happens:**

  * ✅ Full data is persisted to storage
  * ❌ Retrieval is blocked to preserve context space
  * ⚠️ Visual warnings appear in the UI traces panel

  **UI Indicators:**

  * Amber warning badge in timeline: `⚠️ Oversized artifact (~247K tokens)`
  * Detailed warning in expanded view with size and percentage of context

  **Tracing Attributes:**

  * `artifact.is_oversized` - Boolean flag
  * `artifact.retrieval_blocked` - Whether retrieval is blocked
  * `artifact.original_token_size` - Estimated token count
  * `artifact.context_window_size` - Model's context limit

  **Troubleshooting:**
  If seeing many oversized artifacts, consider:

  * Filter tool outputs to return only necessary fields
  * Paginate large result sets
  * Pre-summarize responses in tools
  * Use models with larger context windows
</>

<Callout>
  **Quick Start**: Citation artifacts are automatically included when you create
  a new project with `npx @inkeep/create-agents`. You'll find the complete
  implementation in `artifact-components/citation.ts` in your project. They
  provide a ready-to-use example for source attribution (for example, when
  citing webpages) and are automatically rendered by the Inkeep widget with
  professional styling and interactions.
</Callout>

## How to create an artifact component

1. Go to the Artifact Components tab in the left sidebar. Then select "New artifact component".
2. Add in an id, name, and description. These are required fields.
3. For the props schema, you can either:
   * **Enter a JSON schema** with `inPreview: true` properties to define specific fields that will be immediately available
   * **Leave empty** to save the entire tool result without filtering
4. Click "Submit".

To visually add the artifact component to the Agent, see the [Sub Agents](/visual-builder/sub-agents#adding-artifact-components) page for details.

## Schema Requirements

<Callout type="warning">
  **Critical**: Your artifact schema must match your tool's output structure.
  The system uses JMESPath selectors on JSON - it **cannot extract from free
  text**.
</Callout>

```json
// ❌ WRONG: Tool returns text, schema expects structured data
// Tool output: { "result": { "content": [{ "text": "Weather: Sunny, 75°F" }] } }
{
  "type": "object",
  "properties": {
    "temperature": { "type": "number" }, // ❌ Cannot extract number from text
    "condition": { "type": "string" }     // ❌ Cannot extract condition from text
  }
}

// ✅ CORRECT: Tool returns structured data matching schema
// Tool output: { "result": { "temperature": 75, "condition": "Sunny" } }
{
  "type": "object",
  "properties": {
    "temperature": { "type": "number" },
    "condition": { "type": "string" }
  }
}
```

Use OpenTelemetry traces to debug schema validation issues.

### Preview Fields

Set `inPreview: true` on properties for immediate availability. These fields are:

* Included in the agent's context when an artifact is referenced, so the agent can reason about them directly
* Shown to other agents in the project for cross-agent reasoning
* Streamed in real-time to clients (Vercel AI SDK)
* Auto-rendered by Inkeep's widget (citations as interactive cards)
* Available immediately in UI (full artifact loads on-demand)

Fields **without** `inPreview: true` are persisted in storage but kept out of the agent's working context by default, keeping the context window lean. Agents can retrieve the complete artifact — including all non-preview fields — on demand when they need it.

For more details on how artifact data is streamed via the API, see [Data Artifact Events](/typescript-sdk/data-operations#artifact-events).

**Example:**

```json
{
  "type": "object",
  "properties": {
    "title": {
      "description": "Title of the source document",
      "type": "string",
      "inPreview": true
    },
    "url": {
      "description": "URL of the source document",
      "type": "string",
      "inPreview": true
    },
    "record_type": {
      "description": "Type of record (documentation, blog, guide, etc.)",
      "type": "string",
      "inPreview": true
    },
    "content": {
      "description": "Array of structured content blocks extracted from the document",
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "type": {
            "description": "Type of content (text, image, video, etc.)",
            "type": "string"
          },
          "text": {
            "description": "The actual text content",
            "type": "string"
          }
        },
        "required": ["type", "text"],
        "additionalProperties": false
      }
    }
  },
  "required": ["title", "url", "content", "record_type"],
  "additionalProperties": false
}
```

## Accessing Artifact Data

Agents can access artifact data in a few ways:

* **In context** — after creating or referencing an artifact, the agent can see its preview fields directly in its working context
* **Passing to a tool** — the agent can supply an artifact as an argument to a tool; the tool receives the complete artifact data, including all non-preview fields
* **On-demand retrieval** — the agent can explicitly fetch the full artifact when it needs to read non-preview fields itself

Not every intermediate tool result needs to become an artifact. When an agent chains tool calls — passing one tool's output directly into the next — no artifact is created and nothing surfaces to the user.

## Passing Artifacts to Tools

When a tool receives an artifact as an argument, it has access to all fields — not just the preview ones. For example, a document processing tool can read the full `content` array even though `content` is a non-preview field:

```typescript
import { functionTool } from "@inkeep/agents-sdk";

const summarizeTool = functionTool({
  id: "summarize-document",
  description: "Summarize the content of a document artifact",
  inputSchema: {
    type: "object",
    properties: {
      // The agent passes an artifact here; the tool receives all fields
      document: {
        type: "object",
        properties: {
          title: { type: "string" },
          url: { type: "string" },
          content: {
            type: "array",
            items: {
              type: "object",
              properties: {
                type: { type: "string" },
                text: { type: "string" },
              },
            },
          },
        },
      },
    },
  },
  execute: async ({ document }) => {
    // document.content is fully available here, even though it's non-preview
    const fullText = document.content.map((c) => c.text).join("\n");
    return { summary: fullText.slice(0, 500) };
  },
});
```

Citation artifacts that are named `citation` and have `title` and `url` preview fields are auto-rendered by Inkeep's widget in two ways:

* **Inline footnotes**: Numbered references \[1], \[2], etc. appear inline with the response text
* **Interactive cards**: Source cards with title, URL, and content appear below the response for detailed exploration

<Callout>
  **Important**: The artifact must have a name of `citation` in order to be
  auto-rendered by Inkeep's widget.
</Callout>

<Image src="/images/citation-widget-example.png" alt="Citation artifact rendered as an interactive card in the Inkeep widget showing source attribution" style={{ marginTop: "1rem" }} />

## Artifact Name & Description Generation

**Important**: Artifact names and descriptions are automatically generated at the **agent level** using that specific agent's configured **summarizer model**.

### How It Works

When an agent creates an artifact during tool execution or data processing:

1. **Agent Creates Artifact**: During tool execution or data processing
2. **Summarizer Analysis**: The agent's summarizer model analyzes:
   * The artifact content and structure
   * Recent conversation context
   * The user's original question or request
3. **Name Generation**: Creates a concise, descriptive name (max 50 characters)
4. **Description Generation**: Provides context about relevance and content (max 150 characters)

### Model Settings for Artifacts

Each agent uses its configured summarizer model for artifact generation:

```typescript
// Agent with custom summarizer for artifact generation
const researchAgent = subAgent({
  id: "research-agent",
  name: "Research Agent",
  prompt:
    "You are a research agent that can generate artifact names and descriptions",
  models: {
    base: { model: "anthropic/claude-sonnet-4-0" },
    summarizer: {
      model: "openai/gpt-4.1-mini", // This model generates artifact names/descriptions
      providerOptions: {
        temperature: 0.2, // Lower temperature for consistent naming
        maxOutputTokens: 512,
      },
    },
  },
});
```

### Inheritance and Fallback

Artifact generation follows the same model inheritance rules:

* **Default**: Sub Agent uses the Agent's summarizer model (if configured)
* **Fallback**: If no summarizer configured at project/agent level, falls back to Sub Agent's base model
* **Override**: Agent can specify its own summarizer model

### Best Practices

* **Consistent Models**: Use the same summarizer model across related agents for consistent artifact naming
* **Appropriate Models**: Choose models good at summarization (GPT-4o-mini, claude-3.5-haiku work well)
* **Temperature Settings**: Lower temperatures (0.1-0.3) provide more consistent naming patterns


# Data Components in Visual Builder (/visual-builder/structured-outputs/data-components)

Create and manage data components for rich, interactive agent messages using the Visual Builder.



<>
  ## What are Data Components?

  Data Components allow your agents to return rich, interactive content instead of just text. Think of them as reusable UI building blocks that your agent can populate with data.

  For example, instead of returning "You have 3 tasks: Task #123 (shipped), Task #124 (processing)...", your agent can return a properly formatted task list component.
</>

<>
  ## Model Recommendations

  <Warning>
    **Data components require high-capability models for best results.**

    Configure the structured output model in your agent settings with one of these recommended models:

    | Provider      | Recommended Models                   |
    | ------------- | ------------------------------------ |
    | **Anthropic** | `claude-sonnet-4+`, `claude-opus-4+` |
    | **OpenAI**    | `gpt-4.1`, `gpt-5.1`, `gpt-5.2`      |
    | **Google**    | `gemini-3.0-pro`                     |

    **Not recommended:** Lower-tier models (`gpt-4.1-mini`, `gpt-4.1-nano`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`), or custom models.

    Data components use structured output generation, which requires frontier-level reasoning capabilities.
  </Warning>
</>

## How to create a data component

1. Go to the Data Components tab in the left sidebar. Then select "New data component".
2. Add in an id, name, and description. These are required fields.
3. Enter a JSON schema defining the structure of your data component.
4. Click "Submit".

To visually add the data component to the Agent, see the [Sub Agents](/visual-builder/sub-agents#adding-data-components) page for details.

### Example JSON Schema

```json
{
  "type": "object",
  "properties": {
    "tasks": {
      "type": "array",
      "description": "Array of user tasks",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Task ID" },
          "title": { "type": "string", "description": "Task title" },
          "completed": { "type": "boolean", "description": "Task status" }
        }
      }
    },
    "totalCount": { "type": "number", "description": "Total tasks" }
  }
}
```

## Frontend Integration

Create a React component that receives the props defined by your data component schema:

```tsx
const TaskList = ({ tasks, totalCount }) => (
  <div className="task-list">
    <h3>Your Tasks ({totalCount} total)</h3>
    {tasks.map((task) => (
      <div key={task.id} className={task.completed ? "completed" : ""}>
        {task.title}
      </div>
    ))}
  </div>
);
```

If you have generated component UI (render code) for this data component in the dashboard, you can add the component file to your app instead via the CLI:

```bash
inkeep add --ui <component-id>
```

The CLI writes the component to `apps/agents-ui/src/ui` (or your project’s equivalent).

Register the component with the chat widget. The **key** in `components` must be the component name as shown in the dashboard (exact string), and the **value** is your imported React component:

```tsx
import { TaskList } from './ui/TaskList';

<InkeepSidebarChat
  aiChatSettings={{
    baseUrl: "your-api-server-url",
    appId: "your-app-id",
    components: {
      "Task List": TaskList, // Key = dashboard component name; value = your component
    },
  }}
/>
```

The agent will automatically return structured data that renders as your React component.


# Guardrails in Visual Builder (/visual-builder/structured-outputs/output-contract)

Constrain what a sub agent can emit — require components, artifacts, or a transfer, and forbid free text — using the Guardrails section in the Visual Builder.



Each sub agent has a **Guardrails** section that defines its *output contract*: the set of rules its responses must satisfy. By default a sub agent can respond however it likes — narrate free text, emit a component, or transfer. Guardrails let you declare — and have the framework enforce — that a response takes a specific shape, instead of relying on prompt discipline alone.

This is most useful for multi-stage pipelines (for example `query → select → respond`) where the intermediate stages should hand off structured data rather than narrate to the user.

<Note>
  In the TypeScript SDK, this is the sub agent's [`outputContract`](/typescript-sdk/structured-outputs/output-contract). Guardrails configure the same contract visually.
</Note>

## Open the Guardrails editor

1. Open your agent in the Visual Builder and select a sub agent to open its side panel.
2. Scroll to the **Guardrails** section. Its summary shows what the sub agent can emit (its components and artifacts) and any active rules.
3. Click **Configure** to open the Guardrails dialog.

## Require components or artifacts

The **Components** and **Artifacts** fields hold the [data components](/visual-builder/structured-outputs/data-components) and [artifact components](/visual-builder/structured-outputs/artifact-components) this sub agent can emit. The count badge next to each label reflects how many are selected.

1. Use **Components** / **Artifacts** to add the items this sub agent is allowed to emit.
2. **Click a selected item to require it.** Required items are highlighted and must appear in **every** response (require-all). Click again to make it optional.
3. Removing an item from the inventory also clears its required state.

<Note>
  Selecting a component or artifact only makes it *available* — click the selected chip to toggle whether it is *required*.
</Note>

## Forbid free text

Uncheck **Include text** to stop the sub agent from emitting free-form prose. When unchecked, the sub agent can only respond with the components and artifacts above, a transfer, or tool calls. Leave it checked to allow narration alongside any required items.

## Require a transfer

When the sub agent has at least one transfer target, a **Require transfer** switch appears. Turn it on to make every response hand off to another sub agent instead of replying directly.

<Note>
  **Require transfer** is mutually exclusive with required components or artifacts. A turn ends one way — a transfer hands off, or a component or artifact is emitted, not both.
</Note>

## Choose what happens on a violation

The **On violation** setting controls what the framework does when a response breaks a rule:

| Option               | Behavior                                                                                                                                                                                                    |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Reject** (default) | The run fails with a structured contract-violation error. The error names the sub agent and the rule that was broken, so a chat widget or API caller can surface a useful message instead of a raw failure. |
| **Warn**             | The violation is logged and the response is surfaced as-is. Use this to observe how often a contract would trip before you enforce it.                                                                      |

## Field reference

| Guardrail                         | Output contract field | Effect                                                                                            |
| --------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------- |
| **Components** (click to require) | `requireComponent`    | Every required data component must appear in the response.                                        |
| **Artifacts** (click to require)  | `requireArtifact`     | Every required artifact must be created or referenced.                                            |
| **Include text** (unchecked)      | `allowText: false`    | Free-form prose is forbidden; components, artifacts, transfers, and tool calls are still allowed. |
| **Require transfer**              | `requireTransfer`     | The response must transfer to another sub agent.                                                  |
| **On violation**                  | `onViolation`         | **Reject** fails the run; **Warn** logs the violation and surfaces the response.                  |

## Related

* [Output Contract (TypeScript SDK)](/typescript-sdk/structured-outputs/output-contract)
* [Data Components in Visual Builder](/visual-builder/structured-outputs/data-components)
* [Artifact Components in Visual Builder](/visual-builder/structured-outputs/artifact-components)


# Status Components (/visual-builder/structured-outputs/status-components)

Configure intelligent status updates using structured components in the Visual Builder



The Visual Builder provides powerful tools for configuring status updates across your agents. These features keep users informed during long-running operations with structured component data.

## Default Status Updates

By default, status updates use built-in **default status components** that provide structured labels with contextual details:

* **Label**: User-friendly description like "Searching for information about your query..."
* **Details**: Structured data with operation context, tool usage, and progress information

These are generated automatically by your Agent's summarizer model and provide richer information than plain text summaries.

## Custom Status Components

Status components allow you to define structured data formats for your status updates instead of generic text. This enables richer, more interactive status displays in your UI.

### Creating Status Components

1. **Navigate to your Agent** in the Visual Builder
2. **Open Agent Settings** and find the "Status Updates" section
3. **Enable Status Updates** by configuring trigger conditions
4. **Add Status Components** to define structured update formats

### Example: Tool Call Status Component

Here's an example of a custom status component that provides structured information about tool executions:

```typescript
// Tool Call Summary Component
{
  type: 'tool_call_summary',
  description: 'Summary of a tool execution and its results',
  detailsSchema: {
    type: 'object',
    properties: {
      tool_name: {
        type: 'string',
        description: 'Name of the tool that was executed'
      },
      summary: {
        type: 'string',
        description: 'Brief description of what was accomplished'
      },
    },
    required: ['tool_name', 'summary']
  }
}
```

With this component, instead of generic text, you get structured data like:

```json
{
  "tool_name": "search_knowledge_base",
  "summary": "Found 5 relevant articles about API authentication",
  "duration": 1250
}
```

## AI-Powered Status Generation

Status updates are generated using your Agent's configured **summarizer model**, which analyzes recent agent activity and creates contextual progress messages.

### How It Works

1. **Event Tracking**: The system monitors all agent operations (tool calls, transfers, generations)
2. **Intelligent Triggering**: Updates are sent based on event count or time intervals
3. **AI Analysis**: The summarizer model analyzes recent events and conversation context
4. **Output Generation**: Creates structured status updates with labels and contextual details

### Status Update Modes

* **Default Status Components**: Built-in structured format with `label` and `details` fields
* **Custom Status Components**: Your defined schemas for specific data structures

### Summarizer Model Settings

The summarizer model is configured in your Agent's model settings:

```typescript
// Agent-level model settings
models: {
  base: {
    model: 'anthropic/claude-sonnet-4-5' // Primary model
  },
  summarizer: {
    model: 'openai/gpt-4.1-mini', // Dedicated summarizer
    providerOptions: {
      temperature: 0.3, // Lower temperature for consistent summaries
      maxOutputTokens: 1024
    }
  }
}
```

## Visual Builder Configuration

### Agent-Level Setup

1. **Select your Agent** in the Visual Builder

2. **Open Model Settings**:

   * Set base model for agent conversations
   * Configure summarizer model for status updates
   * Adjust provider options (temperature, tokens)

3. **Configure Status Updates**:
   * **Trigger Events**: Number of operations before sending update
   * **Time Interval**: Seconds between time-based updates
   * **Custom Status Updates Prompt**: Additional context for the summarizer (max 2000 characters)

### Status Updates Prompt

You can provide a custom prompt that gets appended to the base system prompt for status generation. This allows you to:

* Customize the tone and style of status messages
* Add domain-specific context
* Guide the summarizer to focus on particular aspects

```typescript
statusUpdates: {
  numEvents: 3,
  timeInSeconds: 15,
  prompt: `Focus on user-visible progress and avoid technical details.
Keep messages encouraging and professional.
Always mention what information is being found or processed.`
}
```

### Status Component Designer

Use the visual component designer to create structured status updates:

1. **Component Properties**: Define the data structure
2. **Schema Validation**: Ensure consistent output format
3. **Preview Mode**: Test how components will appear
4. **Template Customization**: Style the component display

## Inheritance and Configuration

### Agent-Level Defaults

Status components, summarizer models, and status update prompts are configured at the agent level and apply to all sub-agents in that agent.

## Best Practices

### Model Selection

* **Summarizer Model**: Faster, efficient models (GPT-4.1-mini, Claude-3.5-Haiku) work well for status generation
* **Consistency**: Use the same summarizer across related agents for consistent status updates

### Status Component Design

* **Specific Schemas**: Define precise data structures for reliable output
* **Required Fields**: Mark essential properties as required
* **Descriptive Names**: Use clear component IDs and descriptions

### Custom Prompts

* **Keep it Concise**: Status update prompts are limited to 2000 characters
* **Be Specific**: Guide the model toward the type of information you want highlighted
* **User-Focused**: Emphasize user-visible progress over technical details

### Performance Optimization

* **Event Thresholds**: Balance responsiveness with API usage (3-5 events typically work well)
* **Time Intervals**: Adjust based on typical operation duration (10-30 seconds)
* **Model Efficiency**: Choose cost-effective models for high-frequency status generation

## Next Steps

* [Configure Data Components](/visual-builder/structured-outputs/data-components) for structured agent outputs
* [Set up Artifact Components](/visual-builder/structured-outputs/artifact-components) for rich content handling
* [Manage Credentials](/visual-builder/tools/credentials) for external tool access


# Manage Credentials in Visual Builder (/visual-builder/tools/credentials)

Credentials are used to authenticate MCP servers.



The credentials page allows you to add credentials to your MCP servers.

## Adding a credential

<>
  Make sure your Visual Builder is running (if you haven't set it up yet, see [Quick Start](/get-started/quick-start)). Navigate to the Credentials tab in the left sidebar, click "New credential", and select "Bearer authentication".

  1. Fill in the required credential details:

  | Field     | Description                                                         |
  | --------- | ------------------------------------------------------------------- |
  | `Name`    | A descriptive name for your credential (e.g., "production-api-key") |
  | `API key` | The authentication token or API key for the service                 |

  2. Optionally, configure additional settings:

  | Field                | Description                                                                                                                                                                                               |
  | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `Metadata`           | Additional headers to include with authentication requests (e.g., User-Agent, X-API-Key). Only available for Nango Store.                                                                                 |
  | `Credential store`   | Where to securely store the credential. Choose from available stores ([Nango Store](/typescript-sdk/tools/credentials#nango-store) or [Keychain Store](/typescript-sdk/tools/credentials#keychain-store)) |
  | `Link to MCP server` | Optionally associate this credential with a specific MCP server that does not have a credential configured                                                                                                |

  3. Click "Create Credential" to save the credential.
</>

To use the credential in an MCP server, you can reference it when creating or editing the MCP server. See the [MCP Servers](/visual-builder/tools/mcp-servers) page for more information.

***


# Function Tools in Visual Builder (/visual-builder/tools/function-tools)

Create custom JavaScript functions that your agents can execute directly in the Visual Builder



Function tools allow you to create custom JavaScript functions that agents can execute in secure sandboxes. Unlike [MCP servers](/visual-builder/tools/mcp-servers) that connect to external services, function tools run your own code directly within the agent framework.

## Overview

Function tools are ideal for:

* **Custom business logic** - Implement domain-specific calculations or workflows
* **Data processing** - Transform, validate, or analyze data using JavaScript
* **API integrations** - Connect to services that don't have MCP servers
* **Utility functions** - Create reusable helper functions across your agents
* **Pipeline processing** - Chain tool outputs directly into subsequent tools without storing intermediate results

## Creating Function Tools

Function tools are created within an Agent and can be used by multiple Sub Agents in that Agent.

<Tip>
  If a function tool performs sensitive actions, require explicit user approval before it runs. See
  [Tool approvals](/typescript-sdk/tools/tool-approvals).
</Tip>

### In an Agent

1. **Add a Function Tool Node**: In the Agent editor, drag a "Function Tool" node from the node palette onto the canvas

2. **Configure the Function Tool**: Click on the function tool node to open the configuration panel in the right sidebar:

| Field          | Description                                                   |
| -------------- | ------------------------------------------------------------- |
| `Name`         | A unique name for your function tool (e.g., "calculate\_tax") |
| `Description`  | What the function does and when to use it                     |
| `Code`         | The JavaScript function code to execute                       |
| `Input Schema` | JSON Schema defining the function parameters                  |
| `Dependencies` | NPM packages required by your function                        |

3. **Connect to Agents**: Drag from the function tool node to any agent nodes that should have access to this tool

<Image
  src="/images/function-tool-node-editor.png"
  alt="Function Tool Node Editor"
  style={{
  border: "1px solid #e1e5e9",
}}
/>

### Function Code

Write your JavaScript function in the Code editor. The function receives arguments based on your Input Schema and should return a result:

```javascript
// Example: BMI calculator
function calculateBMI({ weight, height }) {
  const bmi = weight / (height * height);
  let category = "Normal";
  if (bmi < 18.5) category = "Underweight";
  else if (bmi >= 30) category = "Obese";

  return { bmi: Math.round(bmi * 10) / 10, category };
}
```

<Note>
  Code can be synchronous aor asynchronous, we accept any flavor.
</Note>

### Input Schema

Define the parameters your function accepts using JSON Schema. This ensures proper validation and helps agents understand how to use your function:

```json
{
  "type": "object",
  "properties": {
    "weight": {
      "type": "number",
      "description": "Weight in kilograms"
    },
    "height": {
      "type": "number",
      "description": "Height in meters"
    }
  },
  "required": ["weight", "height"]
}
```

The Input Schema field offers two editing modes, controlled by the **JSON** toggle:

* **Visual builder** (toggle off) — Form-based editor for easier schema creation without writing JSON
* **JSON mode** (toggle on) — Raw JSON editing with syntax highlighting for advanced users

### Dependencies

If you need external packages for your code to run, then specify the desired version in dependencies; otherwise we always use the latest version.

```javascript
// Dependencies auto-detected from imports
const axios = require("axios"); // latest axios version
const lodash = require("lodash"); // Uses latest lodash version
```

**Manual Override**: Specify dependencies in the Dependencies field to pin to specific versions:

```json
{
  "axios": "1.6.0",
  "lodash": "4.17.21"
}
```

## Example: Weather API Integration

Here's a complete example of a function tool that fetches weather data:

**Name**: `get_current_weather`

**Description**: `Get current weather information for any city using OpenWeatherMap API`

**Code**:

```javascript
// Simple weather lookup example
async function getWeather({ city }) {
  const axios = require("axios");
  const response = await axios.get(`https://api.weather.com/city/${city}`);

  return {
    city: response.data.city,
    temperature: response.data.temp,
    description: response.data.weather,
  };
}
```

**Input Schema**:

```json
{
  "type": "object",
  "properties": {
    "city": {
      "type": "string",
      "description": "City name"
    }
  },
  "required": ["city"]
}
```

**Dependencies**:

```json
{
  "axios": "^1.6.0"
}
```

## Execution Environment

Function tools run in secure, isolated sandboxes that provide a safe execution environment for your code.

### Sandbox Execution

Functions execute with the following characteristics:

* **Isolated execution** - Each function runs in its own sandbox
* **No file system access** - Cannot read or write outside the sandbox
* **Network access** - Only through explicitly declared dependencies (like axios)
* **Resource limits** - CPU, memory, and execution time constraints enforced
* **Stateless execution** - No data persists between function calls

### Runtime Options

When deploying, you can configure:

* **Runtime**: Node.js 22 or TypeScript
* **Timeout**: Maximum execution time per function call
* **Concurrency**: Number of simultaneous function executions

See [Deploy to Vercel](/deployment/vercel#step-2-configure-sandbox-in-your-application) for deployment-specific configuration.

## Tool Output Pipelines

Agents can pass the output of one tool directly as the input to another — no artifact creation required. This is useful when intermediate results are purely processing steps that don't need to be saved or shown to the user.

For example, a function tool that fetches a webpage can pass its output directly into a second function tool that extracts the relevant content. The intermediate data never surfaces to the user. When designing tools meant to be used in sequence, ensure their return types match the input types of downstream tools.

MCP server tools work the same way — their outputs can be piped into function tools or other MCP tools in a pipeline.

### Best Practices

* **Keep functions simple** - Each function should have a single, clear purpose
* **Minimize dependencies** - Only include packages you actually need for better performance
* **Handle errors** - Always catch and return meaningful error messages
* **Test thoroughly** - Test your functions independently before deploying
* **Clear descriptions** - Write descriptive names and descriptions to help agents use tools effectively
* **Design for composability** - When tools form a pipeline, align return types with downstream input types

Function tools provide powerful customization capabilities while maintaining security and performance through sandboxed execution.


# Register MCP Servers in Visual Builder (/visual-builder/tools/mcp-servers)

Create MCP servers that agents can use to access external services.



MCP servers provide tools that your agents can use to interact with external APIs and services. Once created, these servers are automatically synced to discover available tools, which can then be added to your agents.

<>
  To use MCP servers with your agents, you can:

  * **Connect to Native MCP servers** from providers like Atlassian, Linear, Notion, and more
  * **Use Composio's platform** for access to 10,000+ out-of-box MCP servers
  * **Use Gram** to easily convert existing OpenAPI APIs into MCP Servers
  * **Build Custom MCP servers** for totally custom MCP servers

  See the [MCP Servers tutorial](/guides/mcp-servers/overview) for details on all recommended approaches.

  Once you have access to an MCP server, you can register it with your agents.
</>

## Register an MCP server

1. Go to the **MCP Servers** tab in the left sidebar, then select "New MCP server".

2. Fill in the required server details:

| Field            | Description                         |
| ---------------- | ----------------------------------- |
| `Name`           | Server name                         |
| `URL`            | Endpoint URL of a remote MCP server |
| `Transport Type` | Streamable HTTP or SSE protocol     |

3. Optionally, configure additional settings:

| Field           | Description                                                                                                                                                                                                                                            |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Image URL`     | Custom icon for the server (URL or base64 data URL)                                                                                                                                                                                                    |
| `Custom Prompt` | Additional instructions for agents on how to use tools from this server. This context is provided to agents alongside each tool from this server.                                                                                                      |
| `Credential`    | Authentication credential if the server requires it. Choose from existing credentials, or leave blank and add a credential later. For OAuth 2.1/PKCE flows, leave blank and use the `Click to Login` button in the server details page to authenticate |

4. Click "Create" to create the server.

## Viewing server details

After creating an MCP server, you can view its details:

1. Click on your created MCP server from the list
2. The server details page shows:
   * **Server information** - URL, transport type, creation date, etc.
   * **Status** - healthy, unhealthy, unknown, disabled, or shows a "Click to Login" button if OAuth 2.1/PKCE flow is detected
   * **Active tools** - Currently enabled tools for the server
   * **Available tools** - All tools discovered from the server with descriptions and parameters

<Image
  src="/images/mcp-server-healthy-status.png"
  alt="MCP server healthy status"
  style={{
  width: "60%",
  border: "1px solid #e1e5e9",
  display: "block",
  margin: "0 auto",
}}
/>

<Note>
  Tool discovery requires a successful connection to the MCP server. If the
  server shows "Click to Login" in the status, use the button to initiate the
  authentication process.
</Note>

## Editing server settings

To modify an existing MCP server:

1. From the server details page, click the **Edit** button
2. The edit form shows all the same fields as creation, plus the ability to update **Active Tools**. By default, all available tools are enabled. You can select specific tools to enable.
3. Click **Save** to apply changes

## Tool overrides

For each enabled tool, you can configure overrides to customize how the tool appears to agents:

1. In the **Active Tools** section of the edit form, click the **Override** badge next to any tool
2. Configure the following override options:

| Field          | Description                                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `Display Name` | Custom name for the tool that agents will see (overrides the original tool name)                                   |
| `Description`  | Custom description that explains what the tool does (overrides the original description)                           |
| `Schema`       | Simplified parameter schema using visual builder or JSON. Useful for hiding complex parameters or adding defaults. |

3. Click **Save Override** to apply the customizations

<Note>
  Tool overrides help simplify complex tools for agents by providing clearer names, better descriptions, and simplified parameter schemas. The original tool functionality remains unchanged.
</Note>

<Image
  src="/images/mcp-server-edit-form.png"
  alt="MCP server healthy status"
  style={{
  width: "60%",
  border: "1px solid #e1e5e9",
  display: "block",
  margin: "0 auto",
}}
/>

## Authentication setup

If your MCP server requires authentication, you have three options: use the **"Click to Login"** button to authenticate via OAuth, manually create a credential, or dynamically pass credentials at runtime via headers.

### OAuth 2.1/PKCE authentication

1. Create your MCP server without selecting a credential
2. Go to the server details page and click the **"Click to Login"** button
3. Complete the OAuth authentication flow in your browser
4. The credential will be automatically created and saved to your Nango Store (follow steps to [setup Nango](/typescript-sdk/credentials/nango)). If Nango is not set up, the framework will use the [Keychain Store](/typescript-sdk/credentials/keychain) if available.

<Image
  src="/images/mcp-server-click-to-login.png"
  alt="MCP server click to login"
  style={{
  width: "60%",
  border: "1px solid #e1e5e9",
  display: "block",
  margin: "0 auto",
}}
/>

#### 1-click Install MCP servers

<>
  Here's an example list of popular service-maintained MCP Servers that support **1-click** install via the OAuth 2.1/PKCE authentication flow.

  | Name                                                                                                             | URL                                                                  |
  | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
  | [Atlassian (SSE)](https://support.atlassian.com/rovo/docs/getting-started-with-the-atlassian-remote-mcp-server/) | [https://mcp.atlassian.com/v1/sse](https://mcp.atlassian.com/v1/sse) |
  | [Canva](https://www.canva.dev/docs/connect/canva-mcp-server-setup/)                                              | [https://mcp.canva.com/mcp](https://mcp.canva.com/mcp)               |
  | [Hugging Face](https://huggingface.co/mcp)                                                                       | [https://huggingface.co/mcp?login](https://huggingface.co/mcp?login) |
  | [Intercom](https://developers.intercom.com/docs/guides/mcp)                                                      | [https://mcp.intercom.com/mcp](https://mcp.intercom.com/mcp)         |
  | [Linear](https://linear.app/docs/mcp)                                                                            | [https://mcp.linear.app/mcp](https://mcp.linear.app/mcp)             |
  | [Neon](https://neon.com/docs/ai/neon-mcp-server)                                                                 | [https://mcp.neon.tech/mcp](https://mcp.neon.tech/mcp)               |
  | [Notion](https://developers.notion.com/docs/get-started-with-mcp)                                                | [https://mcp.notion.com/mcp](https://mcp.notion.com/mcp)             |
  | [Prisma](https://www.prisma.io/docs/postgres/integrations/mcp-server#remote-mcp-server)                          | [https://mcp.prisma.io/mcp](https://mcp.prisma.io/mcp)               |
  | [Sentry](https://docs.sentry.io/product/sentry-mcp/)                                                             | [https://mcp.sentry.dev/mcp](https://mcp.sentry.dev/mcp)             |
  | [Vercel](https://vercel.com/docs/mcp/vercel-mcp)                                                                 | [https://mcp.vercel.com](https://mcp.vercel.com)                     |

  To add them to your project:

  1. navigate to **MCP Servers** in the Inkeep Visual Builder.
  2. Click on **New MCP Server**
  3. Choose from the preset list under **Popular Services** or register any under **Custom Server**

  When you register the server, an authentication pop-up will appear and allow you to sign in. Your credentials will automatically be saved to your [Nango Store](/typescript-sdk/credentials/nango) or [Keychain](/typescript-sdk/credentials/keychain) credential store.
</>

### Manual credential setup

1. Create a credential in the [Credentials](/visual-builder/tools/credentials) section. The credential can be API Key, Bearer Token, Basic, OAuth 2.0, etc.
2. When creating or editing your MCP server, select the credential from the dropdown
3. The credential will be automatically used when connecting to the server

### Dynamic credential pass-through

For in-product copilots where each end user has their own credentials for the MCP server, you can pass credentials dynamically at runtime via HTTP headers instead of storing a single static credential.

1. Register the MCP server with **no credential** selected (choose "No Authentication")
2. Define the credential as a required header in your agent's [headers schema](/visual-builder/headers)
3. On the connection between the MCP server and the Sub Agent, configure headers using `{{headers.*}}` templates to forward the value:

```json
{
  "Authorization": "Bearer {{headers.auth-token}}"
}
```

The template is resolved at runtime using the validated header from the incoming request and sent as an outbound header to the MCP server on every tool call.

<Note>
  Header keys are converted to **lowercase** (e.g., `Auth-Token` becomes `auth-token`). Define schema properties to match the lowercased key exactly. See [Headers — Forwarding Headers to MCP Servers](/visual-builder/headers#forwarding-headers-to-mcp-servers) for a full walkthrough.
</Note>

## Finding more remote MCP servers

For additional remote MCP servers you can connect to:

* **[Anthropic's Remote MCP Servers](https://docs.anthropic.com/en/docs/agents-and-tools/remote-mcp-servers)** - Official documentation with curated third-party remote servers
* **[Awesome Remote MCP Servers](https://github.com/jaw9c/awesome-remote-mcp-servers)** - Community-curated list of high-quality remote MCP servers

## Using stdio-based MCP servers

The Visual Builder requires **Streamable HTTP or SSE transport** to connect to MCP servers. If you have an MCP server that runs in **stdio mode** locally, you can use the following setup to connect to it.

### Setup outline

1. **Deploy your stdio -> Streamable HTTP/SSE bridge** (locally or cloud)
2. **Set environment variables** (API tokens, etc.)
3. **Verify it's running** (i.e. on port `4000`, `4001`, etc.)
4. **Add to Visual Builder**:
   * **URL**: `http://localhost:4000/mcp` (or your server URL)
   * **Transport Type**: `Streamable HTTP` or `SSE`

### Example using `mcp-proxy`

1. Get familiar with [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy) - proxy for MCP servers that use stdio transport
2. [Supabase MCP server](https://github.com/supabase-community/supabase-mcp)
3. Run the server locally:

```bash
SUPABASE_ACCESS_TOKEN=YOUR_SUPABASE_ACCESS_TOKEN mcp-proxy --port 4444 --shell "npx -y @supabase/mcp-server-supabase@latest --read-only"
```

4. Add to Visual Builder with the following settings:
   * **URL**: `http://localhost:4444/mcp`
   * **Transport Type**: `Streamable HTTP`

## Adding servers to agents

Once you have created an MCP server, you can add it to your agents:

<Tip>
  For tools that can make destructive changes, require explicit user approval before execution. See
  [Tool approvals](/typescript-sdk/tools/tool-approvals).
</Tip>

1. Go to the [Sub Agents](/visual-builder/sub-agents) page
2. Drag and drop an MCP server into your agent
3. Connect the MCP server to a Sub Agent by dragging from the server's connector to the Sub Agent
4. Your Sub Agent can now use the tools provided by that MCP server

<Image
  src="/images/mcp-server-add-to-agent.png"
  alt="MCP server add to agent"
  style={{
  width: "60%",
  border: "1px solid #e1e5e9",
  display: "block",
  margin: "0 auto",
}}
/>

For detailed instructions on working with MCP servers in agents, see the [Sub Agents](/visual-builder/sub-agents#adding-tools) page.

## Tool Output Pipelines

MCP tool outputs can be passed directly as inputs to subsequent tools — including other MCP servers or function tools — without creating an artifact in between. Agents handle this automatically when tools are designed for sequential use. See [Function Tools — Tool Output Pipelines](/visual-builder/tools/function-tools#tool-output-pipelines) for more detail.


# Tool approvals in Visual Builder (/visual-builder/tools/tool-approvals)

Configure tool approval requirements in the Visual Builder to pause execution until a user approves the request.



Tool approvals let you mark specific tools as requiring user approval before execution. When an agent tries to run a tool that needs approval, execution pauses until your client approves or denies the request.

This is useful for tools that can make destructive changes, access sensitive data, or perform actions that should be reviewed by a human before proceeding.

## MCP tools

For MCP servers, you can configure approval on individual tools or bulk toggle approval for all enabled tools.

<Steps>
  <Step>
    ### Open the MCP server node

    In the Agent Builder canvas, click on an MCP server node that is connected to a Sub Agent. This opens the node editor in the right panel.
  </Step>

  <Step>
    ### Locate the Tool Configuration section

    Find the **Tool Configuration** section. This displays all available tools from the MCP server with checkboxes to enable/disable each tool.
  </Step>

  <Step>
    ### Enable approval for individual tools

    Each enabled tool has a **Needs Approval?** checkbox on the right side. Check this box for any tool that should require user approval before execution.

    <Image
      src="/images/tool-approvals-individual-tool.png"
      alt="Individual tool with Needs Approval checkbox checked"
      style={{
      width: "60%",
      border: "1px solid #e1e5e9",
      display: "block",
      margin: "0 auto",
    }}
    />
  </Step>

  <Step>
    ### Bulk toggle approvals

    Use the checkbox in the **Needs Approval?** header to toggle approval for all enabled tools at once:

    * **Unchecked**: No enabled tools require approval
    * **Indeterminate (dash)**: Some enabled tools require approval
    * **Checked**: All enabled tools require approval

    Clicking the header checkbox toggles between requiring approval for all enabled tools or none.
  </Step>

  <Step>
    ### Save your changes

    Click **Save** to apply your tool approval configuration.
  </Step>
</Steps>

<Note>
  Only enabled tools (checked in the tool list) can have approval configured. Disabled tools are not available to the agent and don't have approval settings.
</Note>

## Function tools

For function tools, approval applies to the entire tool since each function tool node represents a single tool.

<Steps>
  <Step>
    ### Open the function tool node

    In the Agent Builder canvas, click on a function tool node that is connected to a Sub Agent. This opens the node editor in the right panel.
  </Step>

  <Step>
    ### Enable the Require approval setting

    Scroll to find the **Require approval** checkbox. Check it to require user approval before this function tool executes.

    <Image
      src="/images/tool-approvals-function-tool-checkbox.png"
      alt="Function tool settings with Require approval checkbox"
      style={{
      width: "60%",
      border: "1px solid #e1e5e9",
      display: "block",
      margin: "0 auto",
    }}
    />
  </Step>

  <Step>
    ### Save your changes

    Click **Save** to apply your changes.
  </Step>
</Steps>

## Runtime behavior

When a tool with approval enabled is invoked:

1. The agent pauses execution and emits a `tool-approval-request` event in the response stream
2. Your client UI displays the approval request to the user (tool name, inputs, etc.)
3. The user approves or denies the request
4. If approved, the tool executes and the agent continues
5. If denied, the agent receives a human-readable denial reason string (or a default message if no reason was provided), allowing it to understand why the request was denied and adapt its behavior (for example, trying a different approach or asking a clarifying question)

## Best practices

* **Enable approval for destructive operations**: Tools that delete data, modify configurations, or make irreversible changes should require approval
* **Consider data sensitivity**: Tools that access or expose sensitive information may benefit from approval gates
* **Balance UX and safety**: Too many approval prompts can degrade user experience; reserve approval for truly impactful actions
* **Test the approval flow**: Verify your client properly handles approval requests before deploying to production

## Handling approvals in your client

For details on implementing the approval flow in your client application:

* [Tool Approvals (TypeScript SDK)](/typescript-sdk/tools/tool-approvals) — Configure approvals in code and handle approval responses
* [Chat API](/talk-to-your-agents/chat-api#tool-approval) — API payloads for approval requests and responses
* [Custom Tool Approval UI](/talk-to-your-agents/interactive-components#custom-tool-approval-ui) — Replace the default approval card with your own React component using `IkpTool`

## Related

* [MCP Servers](/visual-builder/tools/mcp-servers) — Register and configure MCP servers
* [Sub Agents](/visual-builder/sub-agents) — Connect tools to your agents
* [Function Tools](/visual-builder/tools/function-tools) — Create custom function tools


# User-Level vs Project-Level MCP Servers (/visual-builder/tools/user-vs-project-mcp)

Understand the difference between user-level and project-level MCP servers and how they affect your team workflow



## Overview

When [creating a new MCP server](/visual-builder/tools/mcp-servers) in the Inkeep Visual Builder, you can choose to configure it at the user-level or project-level.

Understanding the difference between these two types is important for managing your team's access to agents and their associated tools.

## User-Level MCP Servers

User-level MCP servers are configured per individual user. When you create a user-level MCP server, each team member must authenticate with their own credentials to use the associated tools.

<img src="/images/user-scope-mcp.png" alt="Inkeep Cloud User-Level MCP Server" style={{ borderRadius: '8px' }} />

### Benefits

* **Personal accounts**: Team members can use their own accounts (e.g., their personal Notion workspace, Google Drive, etc.)
* **Better security**: Credentials are not shared across the team
* **Flexible access**: Each team member can connect to different resources based on their own permissions

### Considerations

* **Individual authentication required**: Each team member must authenticate their own credentials before using an agent that relies on user-level MCPs
* **Setup overhead**: New team members need to complete authentication before they can use shared agents
* **Not compatible with triggers**: User-level MCPs cannot be used with [webhook triggers](/visual-builder/triggers/webhooks) or scheduled jobs because these invocation methods don't have user context

## Project-Level MCP Servers

Project-level MCP servers use shared credentials that are configured at the project level. All team members in the project can use these servers without individual authentication.

<img src="/images/project-scope-mcp.png" alt="Inkeep Cloud Project-Level MCP Server" style={{ borderRadius: '8px' }} />

### Benefits

* **Shared access**: All team members can immediately use agents without additional setup
* **Simplified onboarding**: New team members can start using agents right away
* **Centralized management**: Credentials are managed in one place

### Considerations

* **Shared credentials**: All team members use the same account, which may have security implications
* **Limited flexibility**: Team members cannot use their own accounts or personal resources

### When Sharing an Agent

When sharing an agent with your team, they'll need to authenticate their own credentials for any user-level MCPs before being able to use the agent. Make sure to communicate this requirement to your team members.


# Scheduled Triggers (/visual-builder/triggers/scheduled)

Run your Agents automatically on a recurring schedule or at a specific future time



Scheduled triggers run agents automatically on a recurring schedule (cron) or at a specific future time (one-time). Use them for daily reports, hourly health checks, periodic data syncs, or deferred tasks.

<Warning>
  \*\*User-scoped MCP servers require a configured `runAsUserId`. Without `runAsUserId` set on the trigger, tools that require user-scoped credentials will fail. See [User vs Project MCPs](/visual-builder/tools/user-vs-project-mcp) for more details.
</Warning>

## Create a scheduled trigger

<Steps>
  <Step>
    ### Open the triggers page

    Navigate to your project and select the **Triggers** tab on the left sidebar, then choose **Scheduled**.

    Click **New scheduled trigger**. Select the agent you want to trigger, then click **Continue**.
  </Step>

  <Step>
    ### Configure basic information

    Fill in the required fields in the **Basic Information** section:

    * **Name** — a human-readable identifier (e.g., "Daily Report Generator")
    * **Description** — optional explanation of what the trigger does
    * **Enabled** — toggle on to activate the trigger immediately after creation
  </Step>

  <Step>
    ### Set the execution identity

    In the **Execution Identity** section, choose which user identity the trigger runs as. This determines whose credentials and permissions are used during execution.

    * **None** — the trigger runs without a user identity (tools requiring user-scoped credentials are skipped)
    * **Specific user** — the trigger uses that user's connected credentials (e.g., OAuth tokens for GitHub, Slack, or Jira)

    <Note>
      Non-admin users can only assign triggers to themselves. Org admins can select any user in the organization — and when creating a trigger, admins can select multiple users to bulk-create one trigger per user.
    </Note>
  </Step>

  <Step>
    ### Choose a schedule

    Select the **Schedule Type**:

    <Tabs>
      <Tab title="Recurring (Cron)">
        The schedule builder provides preset frequencies:

        | Frequency                     | You configure                     |
        | ----------------------------- | --------------------------------- |
        | **Every few minutes**         | Interval (e.g., every 15 minutes) |
        | **Every hour**                | Minute past the hour              |
        | **Every day**                 | Time of day                       |
        | **Specific days of the week** | Days + time of day                |
        | **Once a month**              | Day of month + time of day        |
        | **Custom (advanced)**         | Raw 5-field cron expression       |

        The builder shows a live preview of the schedule and auto-detects your browser's timezone.

        <Image src="/images/scheduled-trigger.png" alt="Schedule configuration showing recurring cron with daily frequency, time picker, and timezone" />
      </Tab>

      <Tab title="One-time">
        Pick a future date and time using the date-time picker. The trigger fires once at the specified time and doesn't repeat.
      </Tab>
    </Tabs>
  </Step>

  <Step>
    ### Add a message template and payload (optional)

    Define a text message sent to the agent when the trigger fires.

    When the trigger fires, the agent receives:

    * A **text part** with the interpolated message
    * A **data part** with the full payload object

    If you leave both empty, the agent receives only `{}`.
  </Step>

  <Step>
    ### Configure retry behavior

    Set how the trigger handles failures:

    | Setting         | Range           | Default | Description                         |
    | --------------- | --------------- | ------- | ----------------------------------- |
    | **Max Retries** | 0–10            | 1       | Number of retry attempts on failure |
    | **Retry Delay** | 10–3600 seconds | 60      | Seconds between retries             |
    | **Timeout**     | 30–780 seconds  | 780     | Execution timeout per attempt       |

    Each retry creates a new conversation with the agent. All conversation IDs are tracked on the invocation record.
  </Step>

  <Step>
    ### Save

    Click **Create Scheduled Trigger**. The trigger appears in the **Scheduled** tab and begins running on its configured schedule.
  </Step>
</Steps>

## Manage scheduled triggers

The **Scheduled** tab shows all triggers in your project with their agent, schedule, status, last run, and next run. Each trigger has a dropdown menu with the following actions:

| Action               | Description                                                                                                                                                                    |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Run Now**          | Execute the trigger immediately, regardless of its schedule. Creates a new invocation and runs it with the trigger's configured payload, message template, and retry settings. |
| **View Invocations** | See all past and pending executions for this trigger                                                                                                                           |
| **Edit**             | Update the trigger's name, schedule, template, payload, retry settings, or execution identity                                                                                  |
| **Duplicate**        | Create a new trigger pre-filled with the same configuration                                                                                                                    |
| **Delete**           | Permanently remove the trigger                                                                                                                                                 |

You can also toggle the **Enabled** switch directly in the table to pause or resume a trigger without deleting it.

## Monitor invocations

View invocations for a single trigger via **View Invocations** in the dropdown, or view all invocations across the project by clicking **All Invocations**.

Each invocation shows its status, scheduled time, start time, duration, attempt number, and a link to the conversation trace.

### Invocation actions

| Action     | Available when                        | Description                                                                                       |
| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Cancel** | `pending` or `running`                | Stop the invocation before it completes                                                           |
| **Rerun**  | `completed`, `failed`, or `cancelled` | Re-execute with the same trigger configuration. Creates a new invocation and runs it immediately. |

## Cron expression reference

Cron triggers use standard 5-field syntax:

```
┌───────────── minute (0–59)
│ ┌───────────── hour (0–23)
│ │ ┌───────────── day of month (1–31)
│ │ │ ┌───────────── month (1–12)
│ │ │ │ ┌───────────── day of week (0–7 or MON–SUN)
│ │ │ │ │
* * * * *
```

**Common examples:**

| Expression        | Description                          |
| ----------------- | ------------------------------------ |
| `0 9 * * *`       | Every day at 9:00 AM                 |
| `0 9 * * MON-FRI` | Weekdays at 9:00 AM                  |
| `*/15 * * * *`    | Every 15 minutes                     |
| `0 */2 * * *`     | Every 2 hours                        |
| `0 0 1 * *`       | First day of every month at midnight |

## Next Steps

<Cards>
  <Card title="Define in Code" icon="LuCode" href="/typescript-sdk/triggers/scheduled">
    Create scheduled triggers using the TypeScript SDK.
  </Card>

  <Card title="Webhook Triggers" icon="LuWebhook" href="/visual-builder/triggers/webhooks">
    For event-driven execution from external services, use webhook triggers.
  </Card>

  <Card title="Chat API" icon="LuNetwork" href="/talk-to-your-agents/chat-api">
    For synchronous conversations, use the Chat API.
  </Card>
</Cards>


# Webhook Triggers (/visual-builder/triggers/webhooks)

Create webhook endpoints that let external services invoke your agents via HTTP



Webhook triggers give you an HTTP endpoint that external services — like GitHub, Zendesk, Stripe, or PagerDuty — can POST to.

Incoming payloads are validated, transformed into a message, and used to start a new conversation asynchronously.

<NumberedStepsTOC stepIds={["open-the-triggers-page", "start-a-new-trigger", "configure-and-save", "verify-the-trigger"]} />

## Create a webhook trigger

<Steps>
  <Step>
    ### Open the triggers page

    Navigate to your project and select **Triggers** in the left sidebar, then choose the **Webhooks** tab.

    <Image src="/images/vb-triggers-webhooks/step-1.png" alt="Triggers menu item in the left sidebar" />
  </Step>

  <Step>
    ### Start a new trigger

    Click **New Webhook Trigger**. Select the agent you want the trigger to invoke, then click **Continue**.

    <Image src="/images/vb-triggers-webhooks/step-2.png" alt="Webhooks tab with the New Webhook Trigger button" />
  </Step>

  <Step>
    ### Configure and save

    Fill in the trigger settings described below, then click **Create Trigger**.

    <Image src="/images/vb-triggers-webhooks/step-3.png" alt="Trigger configuration form with name, message template, and authentication fields" />
  </Step>

  <Step>
    ### Verify the trigger

    The new webhook URL appears in the webhooks tab — copy it into your external service's webhook configuration.

    <Image src="/images/vb-triggers-webhooks/step-4.png" alt="Webhooks tab with the new webhook URL" />
  </Step>
</Steps>

## Configuration reference

### Required fields

| Field                | Description                                                                                                                                                             |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name**             | Human-readable identifier (e.g., "GitHub Issues" or "Stripe Payments")                                                                                                  |
| **Message Template** | Converts the webhook payload into a message for the agent. Uses `{{placeholder}}` syntax for interpolation (e.g., `New issue: {{issue.title}} by {{issue.user.login}}`) |

### Optional fields

| Field                | Description                                                                                                                          |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Description**      | Explains what the trigger does                                                                                                       |
| **Enabled**          | Toggle on to activate immediately after creation                                                                                     |
| **Input Schema**     | JSON Schema to validate incoming payloads — rejects requests that don't match                                                        |
| **Authentication**   | One or more custom headers with expected values (see [Authentication](#authentication))                                              |
| **Signing Secret**   | HMAC-SHA256 secret for signature verification (GitHub, Stripe, Slack)                                                                |
| **Output Transform** | Reshape payloads before interpolating the message template (see [Payload transformation](#payload-transformation))                   |
| **Run As Users**     | Execute the trigger as one or more users to access their connected credentials (see [User-scoped execution](#user-scoped-execution)) |
| **Dispatch Delay**   | Optional delay in milliseconds between each per-user execution to smooth out downstream rate limits                                  |

### Message templates

Templates use `{{placeholder}}` syntax to pull values from the (optionally transformed) payload. Nested properties use dot notation:

```
New order received:
- Order ID: {{order.id}}
- Customer: {{customer.name}} ({{customer.email}})
- Total: ${{order.total}}

Please process this order and send a confirmation.
```

When the trigger fires, the agent receives a multi-part message: a **text part** generated from the template and a **data part** containing the full structured payload.

## Authentication

Triggers support flexible header-based authentication. Configure one or more headers that every incoming request must match:

| Approach                          | When to use                                         |
| --------------------------------- | --------------------------------------------------- |
| Custom header (e.g., `X-API-Key`) | Simple API key verification                         |
| `Authorization: Bearer <token>`   | OAuth-style authentication                          |
| Multiple headers                  | Combined verification (e.g., API key + client ID)   |
| Signing secret only               | Services that sign requests (GitHub, Stripe, Slack) |
| No authentication                 | Public endpoints (use with caution)                 |

<Note>
  Header values are securely hashed before storage — original values are never persisted. For services that sign webhooks, you can rely on the signing secret alone for security.
</Note>

## Payload transformation

Transform incoming payloads before they reach the message template. Choose one approach:

| Approach                  | Best for                                                   | Complexity |
| ------------------------- | ---------------------------------------------------------- | ---------- |
| **Object Transformation** | Simple field remapping                                     | Low        |
| **JMESPath**              | Filtering arrays, conditional logic, complex restructuring | High       |

**Object Transformation** maps input fields to output fields. For example, mapping `payload.user.display_name` → `userName` and `payload.action.type` → `actionName`.

**JMESPath** uses a full query language for advanced cases. For example, `{ activeUsers: join(', ', users[?active==true].name) }` filters an array and joins the results. See [jmespath.org](https://jmespath.org/) for the full specification.

<Tip>
  Object Transformation is converted to JMESPath under the hood. Start with object transformation and switch to JMESPath only when you need filtering, conditionals, or array operations.
</Tip>

## User-scoped execution

By default, webhook triggers run without a user identity — tools requiring user-scoped credentials are skipped. Setting **Run As Users** makes the trigger execute as one or more specific users, enabling access to their connected credentials (e.g., per-user OAuth tokens for GitHub, Slack, or Jira).

Each selected user gets a separate execution and invocation record for every webhook request. When a run-as user is set, agents automatically use that user's profile timezone for time-aware responses.

## How webhooks are processed

1. An external service sends an HTTP POST to the trigger's webhook URL
2. Authentication headers or signing secret are verified
3. The payload is validated against the input schema (if configured)
4. The payload is transformed using the output transform (if configured)
5. The message template is interpolated with the (transformed) payload
6. The webhook returns `202 Accepted` immediately with the accepted invocation IDs
7. The agent processes the message asynchronously in a new conversation

## Monitor invocations

View invocations from the trigger's dropdown menu to monitor activity, debug failures, and audit usage.

<Image src="/images/vb-triggers-webhooks/invocations.png" alt="Select view invocations" />

Every webhook invocation is tracked with:

| Field                   | Description                                      |
| ----------------------- | ------------------------------------------------ |
| **Status**              | `pending` → `success` or `failed`                |
| **Run As User**         | Which user identity this invocation executed as  |
| **Request Payload**     | The original webhook body                        |
| **Transformed Payload** | The payload after output transformation          |
| **Conversation ID**     | Links to the conversation the invocation created |
| **Error Message**       | Details if the invocation failed                 |

## Next steps

<Cards>
  <Card title="Define in Code" icon="LuCode" href="/typescript-sdk/triggers/webhooks">
    Create webhook triggers using the TypeScript SDK.
  </Card>

  <Card title="Scheduled Triggers" icon="LuClock" href="/visual-builder/triggers/scheduled">
    Run agents on a recurring schedule or at a specific future time.
  </Card>

  <Card title="Chat API" icon="LuNetwork" href="/talk-to-your-agents/chat-api">
    For synchronous conversations, use the Chat API.
  </Card>
</Cards>


# Draft into a Notion database (/guides/agents/content-writer/draft-into-notion)

Point the Content Writer at a Notion database so drafts land as rows in your internal wiki — ready to review without leaving Notion.



## Overview

If your team lives in Notion, the Content Writer can draft there. Each draft becomes a **new row in a Notion database** — title, summary, source, and a link back to the ticket — so your writers triage and edit without leaving the tool they already use.

## Prerequisites

* Access to Inkeep Enterprise with your **Content Writer** agent provisioned — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet
* A Notion workspace where you can create a database

## Connect Notion

The Notion MCP signs in with a single click — no custom OAuth app to register.

<Steps>
  <Step>
    Register the **Notion MCP server** in your project (server URL `https://mcp.notion.com/mcp`).
  </Step>

  <Step>
    Open the server's page and click **Login**. Authorize Inkeep in the Notion popup and choose the workspace to share.
  </Step>

  <Step>
    Back on the server page, confirm **Active Tools** is greater than zero — the Notion tools are now available.
  </Step>

  <Step>
    On the canvas, connect the Notion MCP to the **Page Drafter** — the sub agent that writes Notion drafts.
  </Step>
</Steps>

## Create the Drafts database

The agent files each draft as a row, so give it a database with the columns it fills in.

<Steps>
  <Step>
    In Notion, create a database (e.g. **Content Writer Drafts**) and share it with your Inkeep connection: open the database → **•••** → **Connections** → add the Inkeep connector.
  </Step>

  <Step>
    Add these properties:

    | Property         | Type   | What it holds                                     |
    | ---------------- | ------ | ------------------------------------------------- |
    | **Name**         | Title  | The draft's title                                 |
    | **Type**         | Select | `New`, `Update`, or `Revision`                    |
    | **Status**       | Select | `Draft` — your team moves it from here            |
    | **Source**       | Select | `Zendesk` or `Slack` — where the signal came from |
    | **Summary**      | Text   | One-line description of what the draft proposes   |
    | **Original URL** | URL    | The existing page being updated (blank for new)   |
    | **Ticket Link**  | URL    | The source ticket (blank if from Slack)           |
  </Step>

  <Step>
    Copy the database's link — in Notion, open the database and click **•••** → **Copy link** — and send it to your Inkeep account team. They point the **Page Drafter** at that database, so new drafts file into it.
  </Step>
</Steps>

Now every draft lands as a row — full content in the page body, metadata in the columns — ready to [review and publish](/guides/agents/content-writer/review-and-publish).


# Automate your docs with the Content Writer (/guides/agents/content-writer/overview)

Turn resolved support tickets and product changes into drafted documentation — a developer-docs PR or a customer Help Center article — with a human approving every change.



## Overview

The **Content Writer** is a background agent that turns the work your team already does — resolving support tickets, merging product changes, answering questions in Slack — into drafted documentation, routed to the right surface: a **developer-docs pull request** or a **draft Help Center article**.

Your Content Writer comes with three sub-agents on its Visual Builder canvas — referenced by name throughout these guides:

* **Router Agent** — decides whether each draft becomes a docs PR or a Help Center article
* **KB Article Agent** — writes the Help Center articles (Zendesk)
* **Page Drafter** — writes the Notion drafts

## What you get

* **Capture novel resolutions from support** — when a rep solves something that isn't in your docs yet, it becomes a drafted update instead of disappearing when the ticket closes.
* **Keep docs in sync with the product** — code changes and releases become drafted doc updates, so docs stop trailing the product.
* **Lift your AI-channel visibility** — more accurate, more complete content is what LLMs and AI search cite back to your users.

## How it ships

The Content Writer is part of **Inkeep Enterprise** — your agent is provisioned for you, already grounded in your knowledge base. [Schedule a demo](https://inkeep.com/schedule-demo) to get access.

<Note>
  The Inkeep team provisions your agent and helps you complete the initial integration setup — GitHub Work App, Zendesk service account, and webhook triggers — in the [Content Writer enterprise setup guide](/deployment/inkeep-enterprise/content-writer). These guides cover how you customize and operate it from there.
</Note>

These guides cover what you configure on top of the delivered agent:

<Cards>
  <Card title="Publish to your Help Center" icon="LuBookOpen" href="/guides/agents/content-writer/publish-to-help-center">
    Connect Zendesk so the agent files customer-facing draft articles.
  </Card>

  <Card title="Draft into Notion" icon="LuFileText" href="/guides/agents/content-writer/draft-into-notion">
    Point it at a Notion database to draft into your internal wiki.
  </Card>

  <Card title="Route drafts" icon="LuSplit" href="/guides/agents/content-writer/route-drafts-to-the-right-surface">
    Tag a ticket or set a rule — send each draft to a docs PR or a Help Center article.
  </Card>

  <Card title="Run it automatically" icon="LuPlay" href="/guides/agents/content-writer/run-it-automatically">
    Wire it to solved tickets and merged PRs so every resolution becomes a draft.
  </Card>

  <Card title="Review & publish" icon="LuCheckCheck" href="/guides/agents/content-writer/review-and-publish">
    The human gate — review, publish, and prove it's working.
  </Card>
</Cards>

For cost and model tuning, see [Tuning agents](/guides/agent-engineering/tuning-agents).


# Publish drafts to your Zendesk Help Center (/guides/agents/content-writer/publish-to-help-center)

Connect your Zendesk Help Center so the Content Writer files customer-facing articles — as staff-only drafts your team reviews before they go live.



## Overview

Connect your Zendesk Help Center and the Content Writer gains a place to write for customers: every customer-facing answer becomes a **draft article**, filed in the right section, waiting for a human to publish. A *"how do I add a teammate?"* resolution turns into a Help Center draft in about 30 seconds — no one writes it from scratch.

## Prerequisites

* Access to Inkeep Enterprise with your **Content Writer** agent provisioned — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet
* A Zendesk org where you're an admin

## Connect the Zendesk MCP

<Steps>
  <Step>
    Connect your Zendesk workspace to the **Zendesk MCP server** in your project. The service account, API token, and required headers are covered in [Content Writer enterprise setup → Zendesk integration](/deployment/inkeep-enterprise/content-writer#zendesk-integration).
  </Step>

  <Step>
    Open the server's page and confirm **Active Tools** lists the four tools the Content Writer uses:

    * `list_sections` — find the section a new article belongs in
    * `list_permission_groups` — keep the draft staff-only
    * `create_article` — create the draft article
    * `get_article` — read an existing article when proposing an update

    If **Active Tools** reads `0 tools`, the connection isn't authenticated yet — finish the connector setup before going further.
  </Step>

  <Step>
    On your Content Writer's canvas, connect the Zendesk MCP to the **KB Article Agent** — the sub agent that writes Help Center articles. (The other sub agents don't need Zendesk access.)
  </Step>
</Steps>

That's it — your Content Writer can now draft into your Help Center.

## What lands

When a ticket is solved, the Content Writer reads the resolution, calls `list_sections` to find the section that best fits the topic, and writes a complete, publish-ready article in valid Help Center HTML. It files that as:

* **A real, customer-ready article** — a how-to or troubleshooting answer written for the reader, not a "gaps for the docs team" summary.
* **An unpublished, staff-only draft** — created with `draft` status and a staff permission group, so it's invisible to customers until a human publishes.
* **A link in Slack** — the agent posts the draft article's URL to your team channel with a one-line summary of what it covers.

From there, [review and publish](/guides/agents/content-writer/review-and-publish).


# Review, approve, and publish (/guides/agents/content-writer/review-and-publish)

Every draft waits for a human. Review it where it already lives — a GitHub PR or a Zendesk draft — then publish, automatically if you want.



## Overview

Nothing the Content Writer makes goes live on its own. A code change becomes an **open pull request**; a customer answer becomes an **unpublished draft**. Both wait for you. The agent does the audit and the first draft; your team does the judgment — and that approval step is the whole point.

## Review where the draft already lives

You don't learn a new tool. Drafts show up where your team already reviews work, linked from Slack:

* **Developer docs → a pull request.** Read the summary, review the diff, leave comments (the agent can revise on a follow-up). **Merge** to publish through your normal docs pipeline, or **close** to reject.
* **Help Center or Notion → a draft** from the **KB Article Agent** or **Page Drafter**. Open it, edit anything directly, check the sources. **Publish** to go live, or leave it / delete it to reject.

## Publish on approval

Once you trust a surface, drop the last manual step:

* **Pull requests** — your repo already governs this. Merging publishes through your existing CI/CD; keep the gate exactly where you want it with branch protection and required reviews.
* **Help Center** — a Zendesk automation can publish (or ping the team) when a reviewer adds an `approved` tag to the draft. A human still decides; you just skip the busywork.

Start with publish-on-click, and automate only the surfaces you've watched long enough to trust.

## Prove it's working

Track three numbers to show the program's impact to leadership:

* **Approval rate** — the share of drafts published vs rejected. A healthy rate means useful drafts; a falling one is an early signal to revisit your routing or sources.
* **Draft volume by source** — tickets vs PRs vs Slack. Shows where your missing-content signal is loudest.
* **Freshness lag** — time from ticket solved / PR merged to the published update. The number that says *"docs keep up with the product."*

You compile these from where the drafts live — published vs. rejected articles in Zendesk, merged vs. closed PRs in GitHub, draft statuses in Notion — rather than from a single built-in dashboard.

## Next steps

* **[Tune cost and models](/guides/agent-engineering/tuning-agents)** — adjust the agent's cost/quality balance.
* **[Customize further](/deployment/inkeep-enterprise/content-writer#customizing-the-content-writer)** — skills, prompts, tools, and triggers on your provisioned agent.


# Send each draft to the right place (/guides/agents/content-writer/route-drafts-to-the-right-surface)

One agent, two destinations. Tag a ticket and your Content Writer drafts a docs PR or a Help Center article — exactly where you want it.



## Overview

The same resolved ticket can belong in two very different places. A bug fix in the CLI belongs in your **developer docs**. A *"how do I invite a teammate?"* belongs in your **Help Center**. Your Content Writer drafts to both — and you decide which one each draft lands in.

This page is about **routing** — choosing which surface each draft goes to.

## Prerequisites

* Access to Inkeep Enterprise with your **Content Writer** agent provisioned — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet
* At least one destination connected: [Help Center](/guides/agents/content-writer/publish-to-help-center) or [Notion](/guides/agents/content-writer/draft-into-notion)

## Route with a tag

The fastest, most predictable control is a **tag on the ticket**. Tag it, and the draft goes exactly where you said — an explicit tag always wins.

| Add this tag in Zendesk | Your Content Writer drafts…             |
| ----------------------- | --------------------------------------- |
| `docs-writer`           | a **pull request** in your docs repo    |
| `kb-writer`             | a **draft article** in your Help Center |

**Try it:** resolve a ticket about inviting a teammate, add the `kb-writer` tag, and mark it **Solved**. Within about 30 seconds a draft Help Center article appears, written from the resolution — and the agent drops the link in your team's Slack channel.

These tag names aren't magic — the **Router Agent** is simply instructed to honor them, so they're yours to rename or extend in its instructions.

## Route by content

Leave a ticket **untagged** and your Content Writer's **Router Agent** decides, from what the resolution is actually about:

* code, API, SDK, config, or release detail → a **docs PR**
* a customer how-to, "how do I…", or troubleshooting answer → a **Help Center article**

That default is good out of the box. To bend it to *your* product, open the **Router Agent** and add your own rules to its routing instructions — plain English, no code:

```text
- Anything about billing, invoices, or refunds → Help Center article
- Anything that names a CLI command or an SDK method → docs PR
- When a ticket fits both, prefer the Help Center (a customer asked it)
```

Now *"my invoice shows the wrong tax"* routes to your Help Center even though no one tagged it — because you taught the agent what your customers ask about.

***

Drafts land on the destinations you've connected — set those up in [Publish to your Help Center](/guides/agents/content-writer/publish-to-help-center) or [Draft into Notion](/guides/agents/content-writer/draft-into-notion). Then [review and publish](/guides/agents/content-writer/review-and-publish) what your routing produces.


# Run it automatically on solved tickets (/guides/agents/content-writer/run-it-automatically)

Wire your Content Writer to solved tickets and merged PRs so every resolution becomes a draft — no one has to remember to ask.



## Overview

The Content Writer earns its keep when nobody has to trigger it. Once it's wired to your support tool and repo, **every solved ticket and merged PR becomes a draft automatically** — the moment someone resolves something new, it's already being written up.

## Prerequisites

* Access to Inkeep Enterprise with your **Content Writer** agent provisioned — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet

## Wire the triggers

The trigger wiring touches your Zendesk and GitHub accounts, so it's set up once in the enterprise setup guide:

* **Zendesk** — a webhook fires the Content Writer when a ticket is **solved** and carries the `inkeep_content_writer` tag. See [enterprise setup → Zendesk integration](/deployment/inkeep-enterprise/content-writer#zendesk-integration).
* **GitHub** — a workflow fires it on a merged pull request (or an `@inkeep` PR comment). See [enterprise setup → GitHub integration](/deployment/inkeep-enterprise/content-writer#github-integration).

Once those are in place, resolving a tagged ticket or merging a PR drafts documentation on its own.

<Tip>
  The `inkeep_content_writer` tag is the gate: only solved tickets carrying it run the Content Writer. That's how you choose exactly which resolutions become drafts — add the tag as part of resolving, or have an upstream rule add it.
</Tip>

## Other ways to feed it

* **Slack** — mention the Content Writer in a thread to hand it knowledge directly: *"we changed the webhook retry behavior — here's the changelog."* It drafts from what you tell it.

## Where it reports

As it works, the Content Writer posts progress and the final draft link to **Slack**. Connect the [Slack work app](/talk-to-your-agents/slack/installation) to your project, then invite it to the channel your docs team watches — type `/invite @Inkeep` in that channel — and that's where it reports.

## Next steps

* **[Review and publish](/guides/agents/content-writer/review-and-publish)** — every draft waits for a human; review it, then publish or reject.


# Build and deploy a Support Copilot for Zendesk (/guides/agents/support-copilot/setup)

Deploy an AI copilot into your Zendesk ticket sidebar that grounds its drafts in your team's internal context — prior resolved threads the customer-facing bot never saw — so agents get answers, sources, and next steps without leaving the ticket.



<img src="/images/guides/support-copilot/ticket-sidebar.png" alt="The Inkeep Agents copilot open in a Zendesk ticket sidebar, analyzing the ticket" width="700" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />

## Overview

> **Who this is for:** support teams giving their **agents** a copilot inside the **Zendesk ticket sidebar**. (For a copilot embedded in your own product for **logged-in end users**, see [In-Product Copilot](/guides/agents/in-product-copilot).)

Support Copilot puts an Inkeep agent right inside the Zendesk ticket sidebar, working alongside your support team. Its edge is the context it can reach that your customer-facing bot never could: your team's **internal** knowledge — prior resolved threads, internal Slack discussions, and the diagnostics behind past fixes.

When an agent opens a ticket and runs Smart Assist, the copilot will:

1. Read the ticket — the conversation, metadata, and customer details — automatically
2. Search your internal threads for the times your team already solved this exact issue
3. Return structured assistance: a **Draft Answer** the agent can send, the **Relevant Sources** behind it (the internal thread *and* the public article that backed the fix), plus quick replies, clarifying questions, a thread summary, or follow-up tasks as the ticket needs

Everything happens inside the ticket the agent is already working in. No tab-switching, no copy-paste.

## Prerequisites

* Access to Inkeep Enterprise — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet
* An internal knowledge source exposed as an MCP server — e.g. your internal Slack support channels or a store of past resolved tickets — that returns prior threads and the public source behind each fix. See [Build Custom MCP Servers](/guides/mcp-servers/custom-mcp-servers) if you need to create one.
* A Zendesk account, with a Zendesk admin available to approve the app install

<Note>
  The internal-knowledge MCP is a custom server you build and deploy — it's where your team's prior resolutions live. Budget a little engineering time for it; everything after it is no-code in the Visual Builder.
</Note>

## Build the Smart Assist agent

Start by building the agent that powers the copilot: a single sub agent that searches your internal threads and helps the support agent reason about the ticket.

<Steps>
  <Step>
    In the Visual Builder, go to the **Agents** tab in the left sidebar and click **Create agent**. Give it a name like `Support copilot`.
  </Step>

  <Step>
    Click the **Default Sub Agent** on the canvas to configure it. Fill in:

    * **Name**: `Smart Assist`
    * **Prompt**: Paste the prompt below
    * **Base model**: Leave the inherited default (e.g., Claude Sonnet 4.5).
  </Step>
</Steps>

**Prompt:**

```text
You are an AI copilot for [Your Company] that helps the support team make progress on customer tickets.

You're assisting a support agent — they are your user. Every message you receive is from the support agent, never from the customer. The ticket's conversation, metadata, and customer details are provided to you automatically. Focus on where the conversation currently stands.

How to help:
1. When the agent asks you to analyze a ticket — "Smart Assist", "what should I do here", "help me with this" — search your internal threads FIRST. Your team may have resolved this exact issue before; that internal context is knowledge the customer-facing bot never had.
2. Surface what you found together in one response: a Relevant Sources card showing the internal thread AND the public source behind the fix, plus a Draft Answer card with a customer-ready reply that cites only the public source.
3. If the internal thread reveals the root cause (e.g. "not an outage — the bot was never invited to the channel"), state that briefly to the agent before the cards.
4. When the customer hasn't given enough detail to resolve it, ask the questions the agent should send instead of drafting.

Rules:
- Only cite a URL if it appears verbatim in a search result — never invent or guess one. An answer with no source beats one with a fabricated source.
- Never put internal/private links or customer PII in customer-facing content. Internal threads are for the agent only.
- Don't tell the customer to "reach out to support" — they already have.
```

<Tip>
  Replace `[Your Company]` with your company name. This prompt is a starting point — tune the tone and the "how to help" steps to match how your team works.
</Tip>

## Connect your internal knowledge

Smart Assist's advantage is the internal context it can reach. Expose that knowledge as an MCP server that gives the agent a search tool — in our example MCP that tool is `search_internal_threads`, returning a prior internal thread plus the public source that backed the fix. Your MCP may name its tool differently.

Each search result should carry the internal thread, a short resolution summary, and the public source URL that backed the fix — for example:

```json
{
  "thread_content": "Customer's Slack bot wasn't responding when tagged…",
  "resolution": "Not an outage — the bot was never invited to the channel. Fix: /invite @bot, then retry.",
  "public_source_url": "https://slack.com/help/articles/..."
}
```

Smart Assist links `public_source_url` in the customer-facing Draft Answer and keeps the internal thread in Relevant Sources, flagged private for the agent.

<Steps>
  <Step>
    Click **MCP Servers** in the left sidebar, then **New MCP Server → Custom Server**. Register your internal-knowledge server and give it a name (e.g., `Internal Slack`).
  </Step>

  <Step>
    Go back to your agent canvas, drag an **MCP** block onto it, select your internal-knowledge server, and connect it to the **Smart Assist** sub agent.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner.
  </Step>
</Steps>

Your canvas should now show **Smart Assist** connected to the internal-knowledge MCP:

<img src="/images/guides/support-copilot/agent-canvas.png" alt="The Smart Assist sub agent connected to the internal-knowledge MCP on the Visual Builder canvas" width="280" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />

<Tip>
  Need to build the internal-knowledge MCP itself? See [Build Custom MCP Servers](/guides/mcp-servers/custom-mcp-servers) for wrapping any internal source — Slack, past tickets, a wiki — as a server the agent can search.
</Tip>

## Add structured assistance

Smart Assist doesn't just reply in plain text — it returns rich cards the agent can act on. You define these as **data components**: name each one and give it a JSON schema, and the agent fills them in. Create them in the **Data Components** tab, then attach them to Smart Assist. See [Data Components](/visual-builder/structured-outputs/data-components) for the full walkthrough.

The two that carry this copilot:

* **Draft Answer** — a customer-ready reply the agent can send as-is, citing only public sources.
* **Relevant Sources** — the evidence behind the draft: the internal thread (marked private, for the agent's eyes only) and the public article (safe to link to the customer).

Add more as your team needs them — the demo also ships **Quick Replies** (one-click acknowledgements), **Questions to Ask** (clarifying questions when the ticket is thin), a **Thread Summary** for long threads, and **Customer / Agent Tasks** for open action items.

<Tip>
  Keep customer-facing cards (Draft Answer, Quick Replies, Questions to Ask) free of internal links and PII. Internal context belongs only in Relevant Sources, flagged private for the agent.
</Tip>

## Create a Support Copilot app

A **Support Copilot** app is how you deploy your agent into an external support platform like Zendesk. It connects your agent to the platform and defines the quick actions your team sees in the sidebar.

<Steps>
  <Step>
    Go to the **Apps** tab in the left sidebar and click **New App → Support Copilot**.

    <img src="/images/guides/support-copilot/new-app-menu.png" alt="The New App menu with the Support Copilot option for deploying agents to support platforms" width="360" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />
  </Step>

  <Step>
    Fill in the app:

    * **Name**: A name your team will recognize (e.g., `Support Copilot`)
    * **Default Agent**: Select the `Support copilot` agent you just built
    * **Platform**: `Zendesk`
    * **Quick actions**: Action buttons shown at the top of the sidebar, organized into named groups. Each button sends a message to the agent on click. Create a group — e.g., **ANALYZE** — with **Smart Assist**, **Summarize thread**, and **Draft reply**.
  </Step>

  <Step>
    Click **Create App**.

    <img src="/images/guides/support-copilot/new-app-form.png" alt="The New Support Copilot form showing name, default agent, Zendesk platform, and quick actions" width="560" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />
  </Step>
</Steps>

<Tip>
  Quick actions are fully customizable — rename them, add your own, or group them. The message a button sends is exactly what your agent receives, so phrase it the way you'd ask ("Draft a reply to this customer").
</Tip>

## Install in Zendesk

With the app created, install it in Zendesk so it appears in the ticket sidebar. You do this once per Zendesk account.

<Steps>
  <Step>
    From the app, open the install instructions and click **Open Zendesk Marketplace**. Add the **Inkeep Agents** app to your Zendesk account. A Zendesk admin must approve the install.
  </Step>

  <Step>
    Open any ticket in Zendesk and log in with your Inkeep account. The app detects your workspace automatically.
  </Step>

  <Step>
    The **Inkeep Agents** app now appears in the ticket sidebar, with your quick actions at the top.
  </Step>
</Steps>

The in-app install dialog walks you through the same three steps:

<img src="/images/guides/support-copilot/install-modal.png" alt="The Install Support Copilot dialog with the three install steps and an admin note" width="520" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />

<Note>
  Only people added to the project (Project Member role or above) can use the Support Copilot app. Org admins have access automatically — add project members to give the rest of your team access.
</Note>

## Use it in a ticket

Open a ticket in Zendesk and find **Inkeep Agents** in the sidebar. The copilot greets you with the ticket it's looking at, and your quick actions are ready at the top.

Click **Smart Assist** and the copilot reads the ticket, searches your internal threads, and — when your team has solved it before — surfaces the prior fix: a brief note on the root cause, the **Relevant Sources** behind it, and a **Draft Answer** grounded in the public source, ready to send. Or type your own message to ask it anything about the ticket.

From here you can extend the copilot with more tools as your team needs them — for example, pulling up a customer's logs or checking live integration status. Each one builds on the same agent you just deployed.


# Build a confidence-gated auto-reply agent for Zendesk (/guides/agents/ticket-workflows/auto-reply)

Reply publicly to the customer when the knowledge base is sure of the answer — and stay quiet when it isn't.



<img src="/images/guides/ticket-workflows/auto-reply-end-state.png" alt="Zendesk ticket showing a public auto-reply to the customer and an internal audit note" width="700" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />

## Overview

In this tutorial, you'll build an Autoreply agent in the Inkeep Visual Builder. When a new ticket lands in Zendesk, the agent will:

1. Read the customer's question
2. Ask a confidence-aware KB tool — it returns an `answer`, a `confidence` level, and `citations`
3. **Always** post an internal audit note with the answer + confidence
4. **Only when confidence is `very_confident`**, also post a public reply to the customer

The contract is: high confidence → customer gets replied to; anything less → a human takes over. You get deflection on the easy stuff and human review on the hard stuff, automatically.

## Prerequisites

* Access to Inkeep Enterprise — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet
* A Zendesk org where the agent can post both internal and public comments
* A **Zendesk MCP server** registered in your project, with tools that post internal comments **and** public comments. If you don't have one yet, see [Build Custom MCP Servers](/guides/mcp-servers/custom-mcp-servers).

## Expose the Inkeep QA confidence tool

Inkeep's QA API answers a question against your knowledge base, and it can call **custom tools you define** as part of answering. You'll use that capability to grade each answer with a `confidence` level and return it alongside the `answer` and its `citations`. That confidence level is what makes safe auto-reply possible — it's how the agent knows whether the KB is sure enough about the answer to send it to a customer.

The five confidence levels:

| Level                | Meaning                                                       |
| -------------------- | ------------------------------------------------------------- |
| `very_confident`     | The KB has a direct answer. Safe to send publicly.            |
| `somewhat_confident` | Partial match. Don't auto-reply — human handles.              |
| `not_confident`      | KB has tangential info at best. Don't auto-reply.             |
| `no_sources`         | KB has nothing relevant. Don't auto-reply.                    |
| `other`              | Anything else (off-topic, too vague, etc.). Don't auto-reply. |

To make this available to your agent, expose it as a tool called `ask_with_confidence(question) → { answer, confidence, citations }`. The cleanest way is a thin Custom MCP that wraps the Inkeep QA API — see [Build Custom MCP Servers](/guides/mcp-servers/custom-mcp-servers) for the Next.js template and the `inkeep add --mcp` CLI flow. Deploy it and grab the public URL.

<Note>
  `confidence` isn't a built-in field — you define it. Inkeep's QA API supports [custom tool calls](https://docs.inkeep.com/cloud/ai-api/question-answer-mode/custom-tool-calls): you register a tool the answering model invokes to grade how well the KB covers the question, and your MCP surfaces that grade as `confidence`. The five levels above are the example taxonomy this tutorial uses — adjust them to fit your bar for sending a public reply.
</Note>

Once it's deployed, register it as a **Custom Server** MCP in the Visual Builder:

<Steps>
  <Step>
    Click **MCP Servers** in the left sidebar, then **New MCP Server → Custom Server**.
  </Step>

  <Step>
    Fill in:

    * **Name**: `Inkeep QA (with confidence)`
    * **URL**: the public URL of your deployed Confidence MCP
    * **Credential**: select your Inkeep API key credential (same one your Unified Search MCP uses — see [Customer Assistant setup](/guides/agents/customer-assistant/setup) if you haven't created it yet)
  </Step>

  <Step>
    Click **Create**.
  </Step>
</Steps>

## Create the Autoreply agent

<Steps>
  <Step>
    In the Visual Builder, go to the **Agents** tab in the left sidebar and click **Create agent**.
  </Step>

  <Step>
    Click the **Default Sub Agent** on the canvas to configure it. Fill in:

    * **Name**: `Autoreply Decider`
    * **Description**: `Always posts an internal audit note. Posts a public reply only when KB confidence is very_confident.`
    * **Prompt**: Paste the prompt below
  </Step>
</Steps>

**Prompt:**

```text
# Your job

For every incoming Zendesk ticket, decide whether it's safe to answer the customer publicly based on KB confidence:

- ALWAYS post an internal audit note (rep-only) with the answer + confidence
- ONLY post a public reply when confidence is `very_confident`

# Workflow

1. Read the customer's question from the trigger message
2. Call **ask_with_confidence** once with the question. It returns `{ answer, confidence, citations }`
3. Post the internal audit note (format below)
4. If `confidence === "very_confident"`, also post a public reply (format below)

# Internal audit note (always)

When `very_confident`:

🤖 INKEEP AUTO-REPLY — Confidence: very_confident ✅
A public reply has been posted to the customer.

Answer:
<answer verbatim from the tool>

Sources:
- <citation 1>
- <citation 2>

Otherwise (any other confidence value):

🤖 INKEEP AUTO-REPLY — Confidence: <value> ⛔
Auto-reply blocked. A human should handle this.

Closest answer the KB had:
<answer verbatim>

# Public reply (only when very_confident)

<answer verbatim>

Sources:
- <citation 1>
- <citation 2>

# Rules

- Call ask_with_confidence exactly once per ticket
- Use the answer verbatim in both comments — don't paraphrase
- Only post a public reply when confidence is exactly `very_confident` — never any other value
- If ask_with_confidence errors, do nothing — don't post any comments
```

<Tip>
  Tool names to verify before you save:

  * `ask_with_confidence` is the example name for the Confidence MCP's tool. If you renamed it during deployment, swap it in the prompt to match.
  * `add_internal_ticket_comment` and `add_public_ticket_comment` are the example names for the Zendesk comment tools. Swap if your MCP names them differently.
</Tip>

## Connect the MCPs

<Steps>
  <Step>
    On the agent canvas, drag and drop an **MCP** block from the top left toolbar. Select **Inkeep QA (with confidence)** and connect it to the **Autoreply Decider** sub agent.
  </Step>

  <Step>
    Click the MCP block and restrict **Active Tools** to only `ask_with_confidence` (it's likely the only tool exposed anyway).
  </Step>

  <Step>
    Drag a second **MCP** block onto the canvas. Select your **Zendesk MCP** and connect it to the **Autoreply Decider** sub agent.
  </Step>

  <Step>
    Click the Zendesk MCP block and restrict **Active Tools** to only the two tools this agent uses: the internal-comment tool and the public-comment tool. Leave everything else off.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner.
  </Step>
</Steps>

## Create the webhook trigger

The trigger is the door Zendesk knocks on to wake the agent up.

<Steps>
  <Step>
    Go to **Triggers** in the left sidebar and click **New Webhook Trigger**.
  </Step>

  <Step>
    Select the **Autoreply Decider** agent you just built as the agent this trigger will invoke, then click **Continue**.
  </Step>

  <Step>
    Fill in:

    * **Name**: e.g. `Zendesk new-ticket trigger (autoreply)`
    * **Authentication**: `None` — the URL itself is the secret
  </Step>

  <Step>
    Click **Create Trigger**. Copy the webhook URL that appears — you'll need it in the next section.
  </Step>
</Steps>

## Wire Zendesk to fire the trigger

Now set up Zendesk so every new ticket POSTs to the trigger URL you just copied.

See [Trigger Inkeep agents from Zendesk tickets](/visual-builder/triggers/ticketing/zendesk) — paste your trigger URL into the Zendesk webhook's **Endpoint URL**. Come back here when you're done.

## Test the agent

<Steps>
  <Step>
    Send a test ticket your KB should be confident about — pick a common FAQ your docs cover (e.g., *"How do I get started with \[your product]?"*). Within \~15 seconds, both a public reply and an internal audit note should appear on the ticket.
  </Step>

  <Step>
    Send a second test ticket your KB shouldn't be confident about (e.g., *"What's the best wifi router for my apartment?"*). Within \~15 seconds, only an internal audit note should appear — marked `Auto-reply blocked.` The customer sees nothing. **That's the gate doing its job.**
  </Step>
</Steps>


# Build an internal-note draft agent for Zendesk (/guides/agents/ticket-workflows/internal-note-draft)

Draft a KB-grounded reply as a private comment on every new Zendesk ticket — the human rep reviews and sends.



## Overview

In this tutorial, you'll build a Reply Drafter agent in the Inkeep Visual Builder. When a new ticket lands in Zendesk, the agent will:

1. Read the customer's question
2. Search your knowledge base for relevant docs
3. Post a draft reply as a **private (internal) comment** on the ticket, with sources cited

The rep opens the ticket and the draft is already there — they review, tweak, and send. The work shifts from *writing* to *reviewing*.

## Prerequisites

* Access to Inkeep Enterprise — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet
* A Zendesk org where the agent can post internal (private) comments on tickets
* A **Zendesk MCP server** registered in your project, with a tool that posts an internal comment on a ticket. If you don't have one yet, see [Build Custom MCP Servers](/guides/mcp-servers/custom-mcp-servers).
* An **Inkeep Unified Search MCP** connected to your knowledge base, with a search tool exposed. If you haven't set this up, follow the [Customer Assistant setup](/guides/agents/customer-assistant/setup) — the credential + MCP registration steps there are the same here.

## Create the Reply Drafter agent

<Steps>
  <Step>
    In the Visual Builder, go to the **Agents** tab in the left sidebar and click **Create agent**.
  </Step>

  <Step>
    Click the **Default Sub Agent** on the canvas to configure it. Fill in:

    * **Name**: `Reply Drafter`
    * **Description**: `Reads a Zendesk ticket, searches the knowledge base, and posts a draft reply as an internal comment.`
    * **Prompt**: Paste the prompt below
  </Step>
</Steps>

**Prompt:**

```text
# Your job

For every incoming Zendesk ticket, search your knowledge base for relevant docs and post a draft reply as a **private (internal) comment** on the ticket. Never reply publicly — the comment is for a human rep to review and send.

# How to draft

1. Read the customer's question from the trigger message
2. Call **search-knowledge-base** to find relevant docs
3. Write a clear, 2-3 paragraph draft reply grounded in what the docs say
4. If the docs don't cover the question, say so plainly in the draft — don't invent facts

# Post the comment

Call **add_internal_ticket_comment** exactly once with:

- `ticket_id` — from the trigger message
- `body` — the draft, formatted like this:

✍️ Draft reply

[Your drafted answer here, 2-3 paragraphs]

Sources:
- [URL 1]
- [URL 2]

— Drafted by AI. Review before sending.
```

<Tip>
  Tool names to verify before you save:

  * `search-knowledge-base` is the example name for the Inkeep Unified Search tool. Your MCP may name it differently — swap it in the prompt to match.
  * `add_internal_ticket_comment` is the example name for the Zendesk internal-comment tool. Same — swap if your MCP names it differently.
</Tip>

## Connect the MCPs

<Steps>
  <Step>
    On the agent canvas, drag and drop an **MCP** block from the top left toolbar. Select your **Inkeep Unified Search MCP** and connect it to the **Reply Drafter** sub agent.
  </Step>

  <Step>
    Click the MCP block and restrict **Active Tools** to only the knowledge-base search tool — the agent doesn't need anything else from this MCP.
  </Step>

  <Step>
    Drag a second **MCP** block onto the canvas. Select your **Zendesk MCP** and connect it to the **Reply Drafter** sub agent.
  </Step>

  <Step>
    Click the Zendesk MCP block and restrict **Active Tools** to only the tool that posts an internal/private comment — this agent only writes internal comments, so leaving other tools enabled invites mistakes.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner.
  </Step>
</Steps>

## Create the webhook trigger

The trigger is the door Zendesk knocks on to wake the agent up.

<Steps>
  <Step>
    Go to **Triggers** in the left sidebar and click **New Webhook Trigger**.
  </Step>

  <Step>
    Select the **Reply Drafter** agent you just built as the agent this trigger will invoke, then click **Continue**.
  </Step>

  <Step>
    Fill in:

    * **Name**: e.g. `Zendesk new-ticket trigger (drafter)`
    * **Authentication**: `None` — the URL itself is the secret
  </Step>

  <Step>
    Click **Create Trigger**. Copy the webhook URL that appears — you'll need it in the next section.
  </Step>
</Steps>

## Wire Zendesk to fire the trigger

Now set up Zendesk so every new ticket POSTs to the trigger URL you just copied.

See [Trigger Inkeep agents from Zendesk tickets](/visual-builder/triggers/ticketing/zendesk) — paste your trigger URL into the Zendesk webhook's **Endpoint URL**. Come back here when you're done.

## Test the agent

<Steps>
  <Step>
    From an outside email address, send a test ticket to your Zendesk support address with a question your knowledge base should be able to answer (e.g., *"How do I get started with \[product]?"*).
  </Step>

  <Step>
    Switch to the Zendesk tab, find the new ticket, and refresh.
  </Step>
</Steps>

Within about 15 seconds, an internal comment appears on the ticket containing the AI's draft reply and the docs it cited. The customer sees nothing yet — the draft is for the rep, not the customer.


# Build a Zendesk ticket-routing agent (/guides/agents/ticket-workflows/routing)

Auto-tag every incoming Zendesk ticket with the team that owns the problem area.



<img src="/images/guides/ticket-workflows/routing-end-state.png" alt="Zendesk ticket showing the routing agent's classification on the ticket" width="700" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />

## Overview

In this tutorial, you'll build a ticket-routing agent in the Inkeep Visual Builder. When a new ticket lands in Zendesk, the agent will:

1. Read the ticket and identify the actual problem
2. Pick the team that owns that problem area
3. Write the team tag to a Zendesk custom field

The result is an inbox that sorts itself.

## Prerequisites

* Access to Inkeep Enterprise — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet
* A Zendesk org where you can create a custom field for ticket routing (e.g., a dropdown field named "Ticket type" with one value per team)
* A **Zendesk MCP server** registered in your project, with a tool that writes a value to a ticket's custom field. If you don't have one yet, see [Build Custom MCP Servers](/guides/mcp-servers/custom-mcp-servers) for how to wrap the Zendesk API as an MCP server.

## Create the Classifier agent

<Steps>
  <Step>
    In the Visual Builder, go to the **Agents** tab in the left sidebar and click **Create agent**.
  </Step>

  <Step>
    Click the **Default Sub Agent** on the canvas to configure it. Fill in:

    * **Name**: `Classifier`
    * **Description**: `Reads a Zendesk ticket, picks the owning team, and writes the team tag to a custom field.`
    * **Prompt**: Paste the prompt below
  </Step>
</Steps>

**About the taxonomy in the prompt:** the `# Team taxonomy` section below is your product map in plain English. When your team reorgs or adds a new product surface, you edit *this* section — no code changes, no retraining. The taxonomy shown is an example for an Inkeep-style team structure; replace it with your own team names and the categories they own.

**Prompt:**

```text
# Your job

For every incoming Zendesk ticket, identify the team that owns the problem area and write that team's tag to the ticket's custom field. One ticket → one custom-field write.

Classify by what the customer needs help with, not by what they wrote about — customers often describe their setup before their actual problem.

# Team taxonomy

- **Product** — questions about how the product works
- **Account** — login, billing, account access
- **Other** — anything off-topic or that doesn't fit

# Write the tag

Call **update_ticket_custom_field** exactly once with:

- `ticket_id` — from the trigger message
- `custom_field_id` — `[YOUR_CUSTOM_FIELD_ID]`
- `value` — `product` for Product, `account` for Account, `other` for Other
```

<Tip>
  Things to substitute before you save:

  * Replace `[YOUR_CUSTOM_FIELD_ID]` with the numeric ID of your Zendesk custom field (find it in **Zendesk Admin → Objects and rules → Fields**).
  * Replace the example team taxonomy + values with your own teams.
  * If your Zendesk MCP names its custom-field-write tool something other than `update_ticket_custom_field`, swap that name in the prompt to match.
</Tip>

## Connect the Zendesk MCP tool

<Steps>
  <Step>
    On the agent canvas, drag and drop an **MCP** block from the top left toolbar.
  </Step>

  <Step>
    Select your **Zendesk MCP** and connect it to the **Classifier** sub agent.
  </Step>

  <Step>
    Click on the MCP block and restrict **Active Tools** to only the tool that updates a ticket's custom field — in our example MCP that's `update_ticket_custom_field`, but your MCP may name it differently. This agent only writes one thing, so leaving other tools enabled invites mistakes.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner.
  </Step>
</Steps>

## Create the webhook trigger

The trigger is the door Zendesk knocks on to wake the agent up.

<Steps>
  <Step>
    Go to **Triggers** in the left sidebar and click **New Webhook Trigger**.
  </Step>

  <Step>
    Select the **Classifier** agent you just built as the agent this trigger will invoke, then click **Continue**.
  </Step>

  <Step>
    Fill in:

    * **Name**: e.g. `Zendesk new-ticket trigger`
    * **Authentication**: `None` — the URL itself is the secret
  </Step>

  <Step>
    Click **Create Trigger**. Copy the webhook URL that appears — you'll need it in the next section.
  </Step>
</Steps>

## Wire Zendesk to fire the trigger

Now set up Zendesk so every new ticket POSTs to the trigger URL you just copied.

See [Trigger Inkeep agents from Zendesk tickets](/visual-builder/triggers/ticketing/zendesk) — paste your trigger URL into the Zendesk webhook's **Endpoint URL**. Come back here when you're done.

## Test the agent

<Steps>
  <Step>
    From an outside email address, send a test ticket to your Zendesk support address with a question that clearly belongs to one of your teams (e.g., *"How do I update my billing details?"* for the Account team).
  </Step>

  <Step>
    Switch to the Zendesk tab, find the new ticket, and refresh.
  </Step>
</Steps>

Within about 10 seconds, the ticket's custom field flips from blank to the matching team value, written by the agent.


# Add citations to your customer assistant (/guides/agents/customer-assistant/citations)

Auto-render inline source pills and a Sources list on every customer-assistant answer.



<img src="/images/guides/customer-assistant/citations-end-state.png" alt="A customer-assistant answer with inline source citations and a Sources list below the response" width="420" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />

## Overview

In this guide, you'll add citations to the customer assistant you built in [Setup](/guides/agents/customer-assistant/setup). Once wired up, every fact the assistant references gets saved as a structured **citation artifact** that the Inkeep widget auto-renders as inline source pills and a Sources list below the answer.

## Prerequisites

* You completed the [Customer Assistant setup](/guides/agents/customer-assistant/setup) tutorial — a working sub agent connected to Inkeep Unified Search.

## Why citations are wired this way

### Why an artifact, not a URL in the text

The easy thing for an AI to do is paste `[Title](URL)` into its response. That breaks down fast:

* The agent can hallucinate URLs that look right but don't exist.
* The widget has no structured handle to render rich source cards from — it would just be a link.
* Other sub agents in your project can't reason about "what sources did the previous agent use" because that information is buried in prose.

An **artifact** is a structured record the agent saves. It has a fixed schema (`title`, `url`, `content`, etc.), gets persisted in storage, and can be referenced by ID across the conversation. The widget knows how to find these and render them.

### Why the artifact must be named `citation`

The Inkeep widget has a built-in contract: any artifact named exactly `citation` with `url` as a preview field gets **auto-rendered** — no frontend code required. The widget produces inline source pills with the response text and a Sources list beneath the answer.

If you name it something else (`source`, `reference`, `link`), the artifact still saves correctly, but the widget won't auto-render it. You'd need to write a custom React component to display it. For a customer assistant, you want the auto-render — name it `citation`.

## Create the citation artifact

<Steps>
  <Step>
    In the Visual Builder, click **Artifact Components** in the left sidebar, then **New Artifact Component**.
  </Step>

  <Step>
    Fill in:

    * **ID**: `citation`
    * **Name**: `citation` *(must match exactly — this is what the widget auto-renders)*
    * **Description**: `Structured factual information extracted from search results`
  </Step>

  <Step>
    Paste the JSON schema below into the **Props schema** field, then click **Save**.
  </Step>
</Steps>

**Props schema:**

```json
{
  "type": "object",
  "properties": {
    "url": {
      "type": "string",
      "description": "URL of the source document",
      "inPreview": true
    },
    "title": {
      "type": "string",
      "description": "Title of the source document",
      "inPreview": true
    },
    "content": {
      "type": "array",
      "description": "Array of structured content blocks from documentation",
      "items": {
        "type": "object",
        "properties": {
          "text": { "type": "string", "description": "The text content with markdown formatting" },
          "type": { "type": "string", "description": "Type of content (text, image, etc.)" }
        }
      }
    },
    "context": {
      "type": "string",
      "description": "Context of the source document",
      "inPreview": true
    },
    "record_type": {
      "type": "string",
      "description": "Type of record (documentation)",
      "inPreview": true
    }
  },
  "required": ["url", "title", "content"]
}
```

## Attach the citation artifact to your sub agent

<Steps>
  <Step>
    Go to your agent canvas: **Agents → Customer assistant**.
  </Step>

  <Step>
    Click the sub agent on the canvas to open its config in the right sidebar.
  </Step>

  <Step>
    Scroll to the **Artifact Components** section and add the `citation` artifact you just created.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner.
  </Step>
</Steps>

## Update the prompt to cite consistently

Attaching the artifact tells the agent the artifact *exists*, but the agent still needs to be told *when and how* to use it. LLMs won't reliably save artifacts unless the prompt explicitly says so — leave it implicit and you'll get inconsistent or missing citations.

Replace your sub agent's prompt with the version below.

**Prompt:**

```text
You are a customer assistant for [Your Product]. Answer customer questions using the official documentation.

How to answer:
1. Always search the knowledge base first to look up relevant docs before answering.
2. Save every fact you reference as a `citation` artifact (`url`, `title`, `content`, `context`, `record_type`) BEFORE using it in your answer.
3. Cite via saved artifact references — never paste raw URLs or `[Title](URL)` lines in the response text.
4. If the docs don't cover it, say so plainly. Don't invent facts.

Tone: Direct, neutral, no fluff. Speak in the first person on behalf of the company.
```

<Tip>
  The phrase "BEFORE using it" matters. Without it, the agent will often write the answer first and skip saving the citation. The widget can only render what was saved as an artifact — so the order matters.
</Tip>

Click **Save Changes** in the top right corner.

## Test your agent

<Steps>
  <Step>
    Click **Try it** in the top right corner to open the chat interface.
  </Step>

  <Step>
    Ask a product question your docs should be able to answer — for example, *"How do I get started?"* or *"What are the main features?"*
  </Step>
</Steps>

## Next steps

* **[Add human escalation](/guides/agents/customer-assistant/escalation)** — when the agent can't help, hand off to a human via a support ticket with full conversation context.


# Add human escalation to your customer assistant (/guides/agents/customer-assistant/escalation)

Teach your assistant when to hand off to a human — and wire it up to your ticketing system so the support team gets the question with full conversation context already attached.



## Overview

Add **human escalation** to the customer assistant you built in [Setup](/guides/agents/customer-assistant/setup). The agent recognizes when it can't help and creates a support ticket with the full conversation as context. A safety-net button at the end lets customers reach a human at any time.

## Prerequisites

* You completed the [Customer Assistant setup](/guides/agents/customer-assistant/setup) tutorial — a working sub agent connected to Inkeep Unified Search.
* Your ticketing system is connected as an MCP server, exposing a `create_support_ticket` tool. We'll treat the MCP as a black box in this guide — only the tool's input shape matters.

## Why escalation works this way

### Why intent-driven is the primary path

A static "Contact support" form makes the customer leave the chat and re-explain their issue from scratch. The agent already knows the question, what it tried, and where it got stuck — so it can create the ticket with all that context already attached. The support rep opens the ticket and sees the full story.

## What your ticketing MCP needs to expose

Throughout this guide, we'll assume your ticketing MCP exposes a tool with this shape:

**Tool name:** `create_support_ticket`

**Inputs:**

```json
{
  "subject": "Short, 1-sentence issue summary",
  "description": "Full conversation context: what the customer asked, what the agent tried, what's still blocking. Quoted user messages where useful.",
  "customer_email": "where the support team should follow up"
}
```

**Output:**

```json
{
  "ticket_id": "TCK-12345",
  "status": "new",
  "requester_email": "customer@example.com"
}
```

If your real MCP has different field names, adjust the prompt and tool call accordingly. The pattern is the same.

## Attach your ticketing MCP to the agent

<Steps>
  <Step>
    Make sure your ticketing MCP is registered in **MCP Servers** with valid credentials (same flow as Inkeep Unified Search in [Setup](/guides/agents/customer-assistant/setup)).
  </Step>

  <Step>
    Go to your agent canvas: **Agents → Customer assistant**.
  </Step>

  <Step>
    Drag and drop an **MCP** block from the top left toolbar onto the canvas. Select your ticketing MCP and connect it to your sub agent.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner.
  </Step>
</Steps>

## Update the prompt to escalate with intent

The agent now *has* the ability to create tickets, but it needs to be told *when* to escalate and *what* to put in the ticket. Without explicit instructions, the agent will either escalate too eagerly or not at all.

**Add these sections to your existing sub agent prompt:**

```text
When to escalate (call `create_support_ticket`):
- The user explicitly asks for a human: "talk to a human", "speak to support", "I need help"
- You searched the knowledge base and found nothing relevant
- The user repeats the same question after you've already answered
- The user expresses frustration or sounds stuck

How to escalate:
1. If you don't have the user's email (no `x-user-email` header), ask once:
   "I'll get this to our support team — what email should they follow up on?"
   Wait for the user's reply before continuing.
2. Once you have the email, call `create_support_ticket` with:
   - `subject`: 1-sentence summary
   - `description`: full conversation context. Quote the user's question; note what you searched, what you found, what's still blocking.
   - `customer_email`: the email you collected (or from `x-user-email`)
3. After the tool returns, confirm:
   "I've opened a support ticket — our team will follow up shortly."
```

<Tip>
  The bullet about the user repeating the same question after you've already answered matters. Without it, the agent keeps re-phrasing the same answer instead of recognizing the loop and escalating.
</Tip>

Click **Save Changes** in the top right corner.

## Test the intent flow

<Steps>
  <Step>
    Click **Try it** in the top right corner to open the chat interface.
  </Step>

  <Step>
    Try each escalation trigger to confirm the agent handles them:

    * **Explicit ask:** *"I need to talk to a human."*
    * **No docs match:** Ask something your knowledge base wouldn't cover (e.g., *"Can you refund my purchase from last March?"*).
    * **Repeat question:** Ask a real product question, then ask the same thing again after the answer.
  </Step>

  <Step>
    In each case, the agent should ask for your email (if not present), then call `create_support_ticket`, then confirm with the follow-up message.
  </Step>
</Steps>

## Add the always-on escalation button

The intent flow handles most cases, but the customer should always be able to reach a human in one click. The Inkeep widget supports custom triggers — you can add a button anywhere in your UI that submits a pre-defined message to the agent.

The pattern, in pseudo-code:

```tsx
// Inside your chat component
const chatRef = useRef<AIChatFunctions | null>(null);

<>
  <InkeepEmbeddedChat
    aiChatSettings={{
      baseUrl: "...",
      appId: "...",
      chatFunctionsRef: chatRef,
    }}
  />
  <button onClick={() => chatRef.current?.submitMessage("I need to talk to a human")}>
    Talk to support
  </button>
</>
```

When the button is clicked, the agent receives "I need to talk to a human" — which is one of the explicit asks in the prompt — and runs the same escalation flow. No separate ticketing path needed.

<Note>
  The exact API depends on which Inkeep widget you're using (Embedded Chat, Chat Button, Side Bar Chat). See the [Chat Components Overview](/talk-to-your-agents) for the full reference.
</Note>


# Build a customer-facing assistant with Inkeep Unified Search (/guides/agents/customer-assistant/setup)

Step-by-step tutorial to build an AI assistant that answers your customers' questions, grounded in your knowledge base via Inkeep Unified Search.



<img src="/images/guides/customer-assistant/agent-canvas.png" alt="Customer Assistant sub agent connected to Inkeep Unified Search MCP on the Visual Builder canvas" width="280" className="mx-auto block" style={{ display: 'block', margin: '0 auto' }} />

## Overview

In this tutorial, you'll build a customer-facing AI assistant in the Inkeep Visual Builder. When a customer asks a question about your product, the agent will:

1. Search your knowledge base via Inkeep Unified Search
2. Answer in plain language, grounded in what your docs actually say

## Prerequisites

* Access to Inkeep Enterprise — [schedule a demo](https://inkeep.com/schedule-demo) if you don't have it yet
* An Inkeep account with [Inkeep Unified Search](/connect-your-data/inkeep) configured for your product (knowledge base, docs, sources connected)

## Create the agent

<Steps>
  <Step>
    In the Visual Builder, go to the **Agents** tab in the left sidebar and click **Create agent**.
  </Step>

  <Step>
    Click the **Default Sub Agent** on the canvas to configure it. Fill in:

    * **Name**: `Customer assistant`
    * **Prompt**: Paste the prompt below
    * **Base model**: Leave the inherited default (e.g., Claude Sonnet 4.5) unless you have a specific reason to change it.
  </Step>
</Steps>

**Prompt:**

```text
You are a customer assistant for [Your Product]. Answer customer questions using the official documentation.

How to answer:
1. Always search the knowledge base first to look up relevant docs before answering.
2. If the docs don't cover it, say so plainly. Don't invent facts.

Tone: Direct, neutral, no fluff. Speak in the first person on behalf of the company.
```

<Tip>
  Replace `[Your Product]` with the name of the product the assistant represents.
</Tip>

## Connect Inkeep Unified Search via MCP

Your agent now needs the ability to actually search your knowledge base. Inkeep Unified Search is exposed as an MCP server you can connect to — once wired up, the agent can call `search-knowledge-base` to ground its answers in your real content.

You'll do this in three parts:

1. Get an API key from your Inkeep Portal
2. Save the API key as a Credential in the Visual Builder
3. Register the Unified Search MCP server in the Visual Builder, then attach it to your agent

### Get an API key from your Inkeep Portal

<Steps>
  <Step>
    Log in to your [Inkeep Portal](https://portal.inkeep.com) and select your project.
  </Step>

  <Step>
    Click **Assistants** in the left sidebar, then **Create assistant → API (Server-side)**.

    <Note>
      The Portal also has an 

      **MCP**

       option in the same dropdown — skip it. That option spins up a new MCP server; we're connecting to Inkeep's existing Unified Search MCP, which just needs an API key for auth. API (Server-side) is what generates that key.
    </Note>
  </Step>

  <Step>
    Name the assistant something memorable (e.g., `customer-assistant`) — make a note of the name, you'll need it to construct the MCP URL in a later step. Then **copy the API key** shown after creation.
  </Step>
</Steps>

### Save the API key as a Credential

<Steps>
  <Step>
    Back in the Visual Builder, click **Credentials** in the left sidebar, then **New Credential**.
  </Step>

  <Step>
    Choose the **Bearer** authentication option and fill in:

    * **Name**: A memorable name (e.g., `inkeep-unified-search`)
    * **API key**: Paste the API key you copied from the Portal
    * **Headers (optional)**: Leave blank

    <Image src="/images/guides/customer-assistant/visual-builder-credential.png" alt="Bearer credential form in the Visual Builder" />
  </Step>

  <Step>
    Click **Create Credential**.
  </Step>
</Steps>

### Register the MCP server and attach it to your agent

<Steps>
  <Step>
    Click **MCP Servers** in the left sidebar, then **New MCP Server → Custom Server**.
  </Step>

  <Step>
    Fill in:

    * **Name**: `Inkeep Unified Search`
    * **URL**: `https://mcp.cloud.inkeep.com/<your-assistant-name>/mcp` — replace `<your-assistant-name>` with the name you gave the assistant in the Portal (e.g., `customer-assistant` → `https://mcp.cloud.inkeep.com/customer-assistant/mcp`)
    * **Credential**: Select the credential you just created

    Click **Create**.
  </Step>

  <Step>
    Go back to your agent canvas: **Agents → Customer assistant**.
  </Step>

  <Step>
    Drag and drop an **MCP** block from the top left toolbar onto the canvas. Select **Inkeep Unified Search** and connect it to your sub agent.
  </Step>

  <Step>
    Click **Save Changes** in the top right corner.
  </Step>
</Steps>

## Test your agent

<Steps>
  <Step>
    Click **Try it** in the top right corner to open the chat interface.
  </Step>

  <Step>
    Ask a product question your docs should be able to answer — for example, *"How do I get started?"* or *"What are the main features?"*
  </Step>
</Steps>

When working correctly, the agent will search your knowledge base and answer grounded in what it finds. If the docs don't cover the question, it will say so rather than guess.

## Next steps

You now have a working customer assistant grounded in your knowledge base.

* **[Add citations](/guides/agents/customer-assistant/citations)** — let customers verify each answer at the source with inline footnotes and source cards.
* **[Add human escalation](/guides/agents/customer-assistant/escalation)** — when the agent can't help, hand off to a human via a support ticket with full conversation context.


# Add your own analytics (/talk-to-your-agents/customization/analytics)

Use our callback function to log events from the search and chat widgets to your own analytics tool.



To understand how usage of the Inkeep search or chat affects your business, you may want to log events from the search and chat widgets to your own analytics tool. This could be Mixpanel, Posthog, Amplitude, Segment or other analytics or CDP tools.

We expose the events through a callback function called `onEvent` that can be configured in the `baseSettings` configuration.

Below is an example of how to use `onEvent` to log all events to your analytics tool.

## Example

```typescript
import { type InkeepCallbackEvent } from "@inkeep/agents-ui-cloud";

const baseSettings = {
  onEvent: async (event: InkeepCallbackEvent) => {
    // console.log('event', event);
    // log the event to your own analytics tool for example PostHog
    posthog.capture(event.eventName, {
      ...event.properties,
    });
  }
};
```

<Callout type="info">
  Analytics SDKs automatically attach the active user identity at capture time, so Inkeep events land on the same user timeline as your existing events — no extra wiring needed for funnels, cohorts, or user paths.
</Callout>


# Content Security Policy (/talk-to-your-agents/customization/content-security-policy)

Configure CSP for Inkeep's UI components.



If your site enforces a Content Security Policy, add the following directives so the
Inkeep widget can load, reach the API, render its default styling, and run its
bot-protection check.

## Directives

The `connect-src`, `style-src`, `font-src`, and `worker-src` directives are required
for all integration methods. `script-src https://cdn.jsdelivr.net` is only needed when
you load the widget via the JS Snippet.

### script-src — widget script

If you load the widget via the [JS Snippet](/talk-to-your-agents/javascript/chat-button):

* `https://cdn.jsdelivr.net`

### style-src — inline styles and default font

The widget injects its styles as inline `<style>` tags, so it requires
`'unsafe-inline'`. Most sites already allow this — common frameworks (Next.js,
Tailwind, styled-components, and similar) need it too — so it is usually already
present in your policy.

* `style-src 'self' 'unsafe-inline' https://fonts.googleapis.com`

`https://fonts.googleapis.com` is only needed for the default Inter font, loaded from
Google Fonts. To avoid that external request, set `disableLoadingDefaultFont: true` in
your widget config and self-host Inter from your own origin — then you can drop
`https://fonts.googleapis.com` here and `https://fonts.gstatic.com` from `font-src`.

### font-src — default font files

When the default font is enabled, the browser fetches Inter font files from:

* `font-src 'self' https://fonts.gstatic.com`

### connect-src — API endpoints

Allowlist the host(s) for the components you use:

* `https://api.agents.inkeep.com` — chat / agents
* `https://api.inkeep.com` — search (SearchBar, search-and-chat)

### worker-src — bot protection

The widget runs a lightweight proof-of-work bot-protection check on a background
Web Worker, which the browser loads from an in-memory `blob:` URL. CSP requires
`blob:` to be listed explicitly — `'self'` alone does not cover it:

* `worker-src 'self' blob:`

### Example

```text
# script-src is only needed for the JS Snippet (omit for npm / React / Next.js):
script-src  'self' https://cdn.jsdelivr.net;
style-src   'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src    'self' https://fonts.gstatic.com;
connect-src 'self' https://api.agents.inkeep.com https://api.inkeep.com;
worker-src  'self' blob:;
```

## Self-hosting the JS file

You can host the widget script on your own server instead of loading it from jsDelivr.

1. Download [`embed.js`](https://cdn.jsdelivr.net/npm/@inkeep/agents-ui-js-cloud@0.17/dist/embed.js) and save it as `inkeep-agents-ui-{version}.js` (find the current version [here](https://cdn.jsdelivr.net/npm/@inkeep/agents-ui-js-cloud@0.17/dist/))
2. Add the file to your project
3. Point the script tag at your copy:

```html
<!-- assumes the file is in the root of your project -->
<script type="module" src="./inkeep-agents-ui-{version}.js" defer></script>
```

4. When updating, re-download the new version and update the script tag.

When self-hosting, you can drop `https://cdn.jsdelivr.net` from `script-src` (the
script is now same-origin). You still need **`worker-src 'self' blob:`** — the
bot-protection worker is bundled inside the script and is always loaded from a
`blob:` URL, regardless of where the script is served.


# Pass context to the widget (/talk-to-your-agents/customization/passing-context)

Send page, session, and user context to your agent so it can give personalized, situation-aware responses.



Give your agent context about who the user is so it can respond more specifically, like their name, plan, current page, or where they are in a flow.

## How context reaches your agent

Context that the agent (the model) should actually read is passed as **HTTP headers** via the widget's `aiChatSettings.headers` prop. You define a headers schema on the agent, and the values become available in two places:

* **In the prompt**, via the `{{headers.<key>}}` template syntax
* **In [context fetchers](/typescript-sdk/context-fetchers)**, which can call your own APIs using header values and expose the result as `{{<variable>.<field>}}`

## Automatic context (timestamp & timezone)

The chat components automatically send the following headers with each request to provide client context:

| Header                      | Description                                       | Example Value              |
| --------------------------- | ------------------------------------------------- | -------------------------- |
| `x-inkeep-client-timestamp` | The client's current timestamp in ISO 8601 format | `2025-01-30T18:15:00.000Z` |
| `x-inkeep-client-timezone`  | The client's timezone identifier                  | `America/New_York`         |

These headers enable your agents to provide time-aware responses. See [Headers](/typescript-sdk/headers#widget-provided-headers) to learn how to use these values in your agent configuration.

## Page & session context (custom headers)

You can pass custom headers to the chat components via the `headers` prop in `aiChatSettings`. Common use cases include passing the current page URL, user context, or session metadata. Custom headers are validated against a schema you define in the agent settings, then made available in your agent prompt via template syntax.

In the following example, you'll pass the current page URL to the agent.

<Steps>
  <Step>
    #### Define the headers schema

    In the Visual Builder, navigate to **Agents** in the left sidebar, select your agent, then open the **Agent Settings** pane and scroll down to **Headers schema** and add the following schema:

    ```json
    {
      "type": "object",
      "properties": {
        "x-current-url": {
          "type": "string",
          "description": "The URL the user is currently viewing."
        }
      },
      "required": []
    }
    ```
  </Step>

  <Step>
    #### Reference the header in your agent prompt

    Use the key defined in your headers schema with the `{{headers.<key>}}` template syntax:

    ```text
    The user is currently viewing {{headers.x-current-url}}.
    ```
  </Step>

  <Step>
    #### Pass the header value from your chat component

    <CodeGroup>
      ```tsx title="React"
      import { InkeepChatButton, type InkeepAIChatSettings } from '@inkeep/agents-ui-cloud'
      import { useState, useEffect } from 'react'

      export default function ChatButton() {
        const [currentUrl, setCurrentUrl] = useState<string>()

        useEffect(() => {
          const sync = () => setCurrentUrl(window.location.href)
          sync()
          window.addEventListener('hashchange', sync)
          window.addEventListener('popstate', sync)
          return () => {
            window.removeEventListener('hashchange', sync)
            window.removeEventListener('popstate', sync)
          }
        }, [])

        const aiChatSettings: InkeepAIChatSettings = {
          baseUrl: 'https://your-api.example.com',
          appId: 'YOUR_APP_ID',
          ...(currentUrl && {
            headers: {
              'x-current-url': currentUrl,
            },
          }),
        }

        return <InkeepChatButton aiChatSettings={aiChatSettings} />;
      }
      ```

      ```js title="JavaScript"
      const chatButton = Inkeep.ChatButton({
        aiChatSettings: {
          baseUrl: 'https://your-api.example.com',
          appId: 'YOUR_APP_ID',
          headers: {
            'x-current-url': window.location.href,
          },
        },
      });

      function syncUrl() {
        chatButton.update({
          aiChatSettings: {
            headers: { 'x-current-url': window.location.href },
          },
        });
      }

      window.addEventListener('hashchange', syncUrl);
      window.addEventListener('popstate', syncUrl);
      ```

      ```tsx title="Next.js"
      'use client';

      import dynamic from 'next/dynamic';
      import type { InkeepChatButtonProps } from '@inkeep/agents-ui-cloud';
      import { usePathname, useSearchParams } from 'next/navigation';

      const InkeepChatButton = dynamic(
        () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepChatButton),
        { ssr: false },
      );

      export default function ChatButton() {
        const pathname = usePathname()
        const searchParams = useSearchParams()

        // Recomputed on every client navigation, since the hooks above re-render the
        // component. Safe to read window here because this component is client-only (ssr: false).
        const search = searchParams.toString()
        const currentUrl =
          typeof window !== 'undefined'
            ? `${window.location.origin}${pathname}${search ? `?${search}` : ''}`
            : undefined

        const buttonProps: InkeepChatButtonProps = {
          aiChatSettings: {
            baseUrl: 'https://your-api.example.com',
            appId: process.env.NEXT_PUBLIC_INKEEP_APP_ID, // Your app ID
            ...(currentUrl && {
              headers: {
                'x-current-url': currentUrl,
              },
            }),
          },
        };
        return <InkeepChatButton {...buttonProps} />;
      }
      ```
    </CodeGroup>
  </Step>
</Steps>

<Note>
  Update header values whenever the context changes — on client-side navigation, after login, or when the user advances a step. In React, derive them from state/router so a re-render passes new values; with the JavaScript API, call `.update({ aiChatSettings: { headers: { ... } } })` (as shown above).
</Note>

## User context (name, plan, onboarding step)

User attributes work exactly like the page example above — declare each key in your headers schema, reference it in the prompt, and pass it from the widget. The values come from your app's auth or session state rather than the browser. This example passes the user's name and plan; add more keys (like `x-onboarding-step`) the same way.

<Steps>
  <Step>
    #### Define the headers schema

    In the **Agent Settings** → **Headers schema**, add a property for each attribute you want to pass:

    ```json
    {
      "type": "object",
      "properties": {
        "x-user-name": {
          "type": "string",
          "description": "The user's display name."
        },
        "x-user-plan": {
          "type": "string",
          "description": "The user's subscription plan, e.g. free or pro."
        }
      },
      "required": []
    }
    ```
  </Step>

  <Step>
    #### Reference the headers in your agent prompt

    Use the keys from your headers schema with the `{{headers.<key>}}` template syntax:

    ```text
    You are helping {{headers.x-user-name}} on the {{headers.x-user-plan}} plan.
    ```
  </Step>

  <Step>
    #### Pass the header values from your chat component

    <CodeGroup>
      ```tsx title="React"
      import { InkeepChatButton, type InkeepAIChatSettings, type InkeepBaseSettings } from '@inkeep/agents-ui-cloud'

      // `user` comes from your auth/session state (props, context, or a hook like useUser()).
      export default function ChatButton({ user }: { user?: { name: string; plan: string } }) {
        const aiChatSettings: InkeepAIChatSettings = {
          baseUrl: 'https://your-api.example.com',
          appId: 'YOUR_APP_ID',
          ...(user && {
            headers: {
              'x-user-name': user.name,
              'x-user-plan': user.plan,
            },
          }),
        }

        const baseSettings: InkeepBaseSettings = {
          // Associate analytics events with this user — pass userProperties.
          ...(user && {
            userProperties: {
              name: user.name,
              plan: user.plan,
            },
          }),
        }

        return <InkeepChatButton baseSettings={baseSettings} aiChatSettings={aiChatSettings} />;
      }
      ```

      ```js title="JavaScript"
      // `currentUser` comes from your app's auth/session state.
      const chatButton = Inkeep.ChatButton({
        baseSettings: {
          // Associate analytics events with this user — pass userProperties.
          userProperties: {
            name: currentUser.name,
            plan: currentUser.plan,
          },
        },
        aiChatSettings: {
          baseUrl: 'https://your-api.example.com',
          appId: 'YOUR_APP_ID',
          headers: {
            'x-user-name': currentUser.name,
            'x-user-plan': currentUser.plan,
          },
        },
      });

      // Refresh the values after the user logs in or changes plan.
      function syncUser(user) {
        chatButton.update({
          baseSettings: {
            userProperties: {
              name: user.name,
              plan: user.plan,
            },
          },
          aiChatSettings: {
            headers: {
              'x-user-name': user.name,
              'x-user-plan': user.plan,
            },
          },
        });
      }
      ```
    </CodeGroup>
  </Step>
</Steps>

For richer data, use a **[context fetcher](/typescript-sdk/context-fetchers)**: pass an identifier like `x-user-id` as a header, and have the agent fetch the full user record from your API at conversation start. The fetched fields are then available in the prompt as context variables.


# Style Components (/talk-to-your-agents/customization/styling)

Customize the appearance of Inkeep's components.



The style configuration provides comprehensive settings for customizing the appearance of Inkeep widgets. This includes theming, typography, spacing, and component-specific styles.

## Theme Configuration

The theme configuration is defined through the `UserTheme` interface which extends `Partial<IkpTheme>`. This allows for deep customization of the widget's appearance.

```typescript
import { type InkeepBaseSettings } from "@inkeep/agents-ui-cloud";

const baseSettings: InkeepBaseSettings = {
  theme: {
    // Basic color customization
    primaryColors: {
      primary: "#26D6FF",
      secondary: "#6366F1",
    },

    // Syntax highlighter themes
    syntaxHighlighter: {
      lightTheme: themes.github,
      darkTheme: themes.dracula,
    },

    // Custom styles injection
    styles: [
      {
        key: "google-fonts",
        type: "link",
        value:
          "https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap",
      },
      {
        key: "custom-theme",
        type: "style",
        value: `
          .ikp-ai-chat-message {
            border-radius: 8px;
            padding: 12px;
          }
          [data-theme='dark'] .ikp-ai-chat-message {
            background: #2D3748;
          }
        `,
      },
    ],

    // CSS variables container class name
    varsClassName: "ikp-variables",

    // Disable loading the default font for the widgets
    disableLoadingDefaultFont: true,
  },
};
```

### Theme Properties

#### Primary Colors

The `primaryColors` property allows you to define the color variations used throughout the widget. It accepts the following color options:

```typescript
primaryColors: {
  textBold?: string         // Bold text color
  textSubtle?: string       // Subtle text color
  lighter?: string          // Lightest shade
  light?: string           // Light shade
  lightSubtle?: string     // Subtle light shade
  medium?: string          // Medium shade
  mediumSubtle?: string    // Subtle medium shade
  strong?: string          // Strong shade
  stronger?: string        // Stronger shade
  textColorOnPrimary?: string     // Text color on primary background
}
```

All colors should be provided as valid CSS color values (hex, rgb, hsl, etc). Each property is optional, allowing you to customize only the specific color variations you need.

#### Syntax Highlighter Theme

The `syntaxHighlighter` property lets you configure different themes for code blocks in light and dark modes.

By default, the Prism syntax highlighting theme we use is [oneLight](https://github.com/FormidableLabs/prism-react-renderer/blob/master/packages/prism-react-renderer/src/themes/oneLight.ts) in light mode and [vsDark](https://github.com/FormidableLabs/prism-react-renderer/blob/master/packages/prism-react-renderer/src/themes/vsDark.ts) in dark mode. To use a different theme, you can provide it in the theme.syntaxHighlighter prop.

```typescript
syntaxHighlighter: {
  lightTheme: themes.github,  // Theme for light mode
  darkTheme: themes.dracula   // Theme for dark mode
}
```

You can use published Prism themes or [create your own](https://github.com/PrismJS/prism-themes).

#### Custom Styles

The `styles` property allows injection of custom styles through two methods:

1. External Resources (type: 'link'):

```typescript
{
  key: 'google-fonts',
  type: 'link',
  value: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'
}
```

2. Inline CSS (type: 'style'):

```typescript
{
  key: 'custom-theme',
  type: 'style',
  value: `
    .ikp-ai-chat-message {
      border-radius: 8px;
      padding: 12px;
    }
  `
}
```

Each style entry requires:

* `key`: A unique identifier to prevent duplicates and enable updates
* `type`: Either 'link' for external resources or 'style' for inline CSS
* `value`: The style content (URL for links or CSS code for styles)

#### Variables Class Name

The `varsClassName` property specifies the class name for the container that holds CSS variables:

```typescript
varsClassName: "ikp-variables"; // Default value
```

This class is added to a wrapper div that provides theming context.

## Color Mode Configuration

The color mode configuration allows you to control how the widget handles light and dark modes. This is configured through the `colorMode` property in the base settings:

```typescript
const baseSettings: InkeepBaseSettings = {
  colorMode: {
    // Force a specific color mode
    forcedColorMode?: string,

    // Enable system color mode detection
    enableSystem?: boolean,

    // Sync with external element
    sync?: {
      // Element to watch for color mode changes
      target: HTMLElement | string,

      // Attributes to monitor
      attributes: string[],

      // Function to determine dark mode
      isDarkMode: (attributes: Record<string, string | null>) => boolean,

      // Optional callback for color mode changes
      onChange?: (colorMode: string) => void
    }
  }
}
```

### Color Mode Properties

#### Forced Color Mode

Use `forcedColorMode` to explicitly set the color mode for the current page:

```typescript
colorMode: {
  forcedColorMode: "dark"; // Forces dark mode
}
```

#### System Color Mode

Enable `enableSystem` to automatically switch between dark and light modes based on the user's system preferences:

```typescript
colorMode: {
  enableSystem: true; // Follows system preference
}
```

#### Color Mode Sync

The `sync` property allows you to synchronize the widget's color mode with your application's theme:

```typescript
colorMode: {
  sync: {
    // Watch the document root for theme changes
    target: document.documentElement,

    // Monitor the data-theme attribute
    attributes: ['data-theme'],

    // Determine dark mode based on attribute value
    isDarkMode: (attrs) => attrs['data-theme'] === 'dark',

    // Optional: Handle color mode changes
    onChange: (mode) => console.log(`Color mode changed to: ${mode}`)
  }
}
```

Common sync configurations:

1. **Data Attribute Sync**:

```typescript
{
  target: document.documentElement,
  attributes: ['data-theme'],
  isDarkMode: (attrs) => attrs['data-theme'] === 'dark'
}
```

2. **Class-based Sync**:

```typescript
{
  target: document.documentElement,
  attributes: ['class'],
  isDarkMode: (attrs) => attrs['class']?.includes('dark-mode')
}
```

## Using CSS Variables

Inkeep provides a set of CSS variables that you can use to customize the widget's appearance. These variables are prefixed with `--ikp-` and are available within the scope of the widget.

### Basic Usage

You can use Inkeep CSS variables within your stylesheets to customize elements. For example:

```css
.ikp-chat-button__button {
  background: var(--ikp-colors-inkeep-primary-medium);
}
```

### Dark Mode Styling

To apply specific styles for dark mode, use the `[data-theme='dark']` attribute selector:

```css
[data-theme="dark"] .ikp-chat-button__button {
  background: #353e52;
}
```

## Customizing Icons

### AI Assistant Avatar

The AI assistant avatar can be customized through the `aiChatSettings` using the `aiAssistantAvatar` property:

it's either a string URL or an object to specify light and dark mode avatars:

```typescript
aiAssistantAvatar: {
  light: string;
  dark?: string;
}
```

### User Avatar

The user avatar can be customized using the `userAvatar` property in the `aiChatSettings`.

```typescript
import { type InkeepAIChatSettings } from "@inkeep/agents-ui-cloud";

export const aiChatSettings: InkeepAIChatSettings = {
  aiAssistantAvatar: "http://example.com/assistant-avatar.png",
  userAvatar: "http://example.com/user-avatar.png",
  // ...rest of aiChatSettings
};
```

### Custom Icons

You can customize various icons throughout the widget by providing a `customIcons` dictionary in the `baseSettings`. Each icon can be either a built-in icon or a custom SVG/image.

#### Available Icons

| Icon Name           | Description                                                                                  |
| ------------------- | -------------------------------------------------------------------------------------------- |
| search              | Search icon in search bar                                                                    |
| thumbsUp            | Thumbs up icon                                                                               |
| thumbsDown          | Thumbs down icon                                                                             |
| messageCopy         | Copy message icon                                                                            |
| codeCopy            | Copy code icon in code header                                                                |
| openLinkInNewTab    | Used on hover in chat citations and search results (when `shouldOpenLinksInNewTab` is true)  |
| openLinkInSameTab   | Used on hover in chat citations and search results (when `shouldOpenLinksInNewTab` is false) |
| breadcrumbSeparator | Breadcrumb separator icon                                                                    |
| switchToSearch      | Switch to search icon                                                                        |
| switchToChat        | Switch to chat icon                                                                          |
| chatSubmit          | Chat submit icon                                                                             |
| close               | Close icon                                                                                   |
| info                | Info icon next to the disclaimer and chat mode toggle                                        |
| command             | Command icon in search bar                                                                   |

#### Example Configuration

```typescript
import { type InkeepBaseSettings } from "@inkeep/agents-ui-cloud";

export const baseSettings: InkeepBaseSettings = {
  customIcons: {
    // Using a built-in icon
    search: {
      builtIn: "IoSearch",
    },

    // Using custom SVG/image URLs
    thumbsUp: {
      custom: "/path/to/thumbs-up.svg",
    },
    thumbsDown: {
      custom: "/path/to/thumbs-down.svg",
    },
    messageCopy: {
      custom: "/path/to/message-copy.svg",
    },
    codeCopy: {
      custom: "/path/to/code-copy.svg",
    },
    openLinkInNewTab: {
      custom: "path/to/open-link-in-new-tab.svg",
    },
    openLinkInSameTab: {
      custom: "path/to/open-link-in-same-tab.svg",
    },
    breadcrumbSeparator: {
      custom: "path/to/breadcrumb-separator.svg",
    },
    switchToSearch: {
      custom: "/path/to/switch-to-search.svg",
    },
    switchToChat: {
      custom: "/path/to/switch-to-chat.svg",
    },
    chatSubmit: {
      custom: "/path/to/chat-submit.svg",
    },
    close: {
      custom: "/path/to/close.svg",
    },
    info: {
      custom: "/path/to/info.svg",
    },
    command: {
      custom: "/path/to/command.svg",
    },
  },
  // ...rest of baseSettings
};
```

> If the url for a `custom` icon is a `.svg` file it will be rendered as an `<svg>`, all other file types will be rendered using an `<Image>` tag.

## Default Theme

The default theme includes these core settings:

### Typography

```typescript
fontFamily: {
  heading: "'Space Grotesk', system-ui, sans-serif",
  body: "'Inter', system-ui, sans-serif",
  mono: "'Fira Code', monospace",
},
fontSize: {
  '3xs': '0.45rem',
  '2xs': '0.625rem',
  xs: '0.75rem',
  sm: '0.875rem',
  md: '1rem',
  lg: '1.125rem',
  xl: '1.25rem',
  '2xl': '1.5rem',
  '3xl': '1.875rem',
  '4xl': '2.25rem',
  '5xl': '3rem',
  '6xl': '3.75rem',
  '7xl': '4.5rem',
  '8xl': '6rem',
  '9xl': '8rem',
}
```

### Z-Index

```typescript
zIndex: {
  hide: -1,
  auto: 'auto',
  base: 0,
  docked: 10,
  dropdown: 1000,
  sticky: 1100,
  banner: 1200,
  overlay: 1300,
  modal: 1400,
  popover: 1500,
  skipLink: 1600,
  toast: 1700,
  tooltip: 1800,
}
```

## Component Class Names

All component class names are prefixed with `ikp-`. Here's the complete list of available class names:

<>
  ### Chat Components

  ```css
  ikp-ai-chat-wrapper
  ikp-ai-chat-root
  ikp-ai-chat-header
  ikp-ai-chat-header__toolbar
  ikp-ai-chat-header__toolbar-header
  ikp-ai-chat-header__toolbar-header-wrapper
  ikp-ai-chat-header__chat-history-button-icon
  ikp-ai-chat-header__chat-history-button
  ikp-ai-chat-content
  ikp-ai-chat-content-scroll-area
  ikp-ai-chat-content-scroll-area__viewport
  ikp-ai-chat-content-scroll-area__scrollbar
  ikp-ai-chat-content-scroll-area__thumb
  ikp-ai-chat-content-scroll-area__corner
  ikp-ai-chat-disclaimer
  ikp-ai-chat-disclaimer-label
  ikp-ai-chat-disclaimer-trigger
  ikp-ai-chat-disclaimer-content
  ikp-ai-chat-disclaimer-text
  ikp-ai-chat-disclaimer-arrow
  ikp-ai-chat-example-questions
  ikp-ai-chat-example-questions-label
  ikp-ai-chat-example-questions-list
  ikp-ai-chat-example-question
  ikp-ai-chat-example-question-button
  ikp-ai-chat-messages
  ikp-ai-chat-message-wrapper
  ikp-ai-chat-message-header
  ikp-ai-chat-message-loading
  ikp-ai-chat-message-avatar
  ikp-ai-chat-message-avatar-fallback
  ikp-ai-chat-message-avatar-image
  ikp-ai-chat-message-avatar-content
  ikp-ai-chat-message-name
  ikp-ai-chat-message-content-wrapper
  ikp-ai-chat-message-content
  ikp-ai-chat-message__files
  ikp-ai-chat-message__file
  ikp-ai-chat-message-part
  ikp-ai-chat-message-toolbar
  ikp-ai-chat-message-custom-actions
  ikp-ai-chat-message-custom-action
  ikp-ai-chat-message-action
  ikp-ai-chat-message-sources
  ikp-ai-chat-message-sources__header
  ikp-ai-chat-message-sources__list
  ikp-ai-chat-message-source-item
  ikp-ai-chat-message-source-item__icon
  ikp-ai-chat-message-source-item__breadcrumbs
  ikp-ai-chat-message-source-item__breadcrumb
  ikp-ai-chat-message-source-item__breadcrumb-icon
  ikp-ai-chat-message-source-item__title
  ikp-ai-chat-message-source-item__tag
  ikp-ai-chat-message-source-item__description
  ikp-ai-chat-message-source-item__description-part
  ikp-ai-chat-message-source-item__indicator
  ikp-ai-chat-footer
  ikp-ai-chat-input__fieldset
  ikp-ai-chat-input__group
  ikp-ai-chat-input
  ikp-ai-chat-input__send-button
  ikp-ai-chat-input__send-button-icon
  ikp-ai-chat-input__file-upload-button
  ikp-ai-chat-input__file-upload-button-icon
  ikp-ai-chat-input__file-input
  ikp-ai-chat-input__file-list
  ikp-ai-chat-input__file-item
  ikp-ai-chat-input__file-preview-trigger
  ikp-ai-chat-input__file-content
  ikp-ai-chat-input__file-thumbnail
  ikp-ai-chat-input__file-name
  ikp-ai-chat-input__file-remove
  ikp-ai-chat-input__file-remove-icon
  ikp-ai-chat-input__drop-zone
  ikp-ai-chat-input__notification
  ikp-ai-chat-input__notification-content
  ikp-ai-chat-input__notification-header
  ikp-ai-chat-input__notification-title
  ikp-ai-chat-input__notification-message
  ikp-ai-chat-input__notification-close
  ikp-ai-chat-action-bar
  ikp-ai-chat__chat-actions
  ikp-ai-chat__chat-action
  ikp-ai-chat__chat-action-label
  ikp-ai-chat__chat-action-feeback
  ikp-ai-chat-help-actions
  ikp-ai-chat-help-action
  ikp-ai-chat-help-actions__trigger
  ikp-ai-chat-help-actions__menu
  ikp-ai-chat-help-actions__menu-arrow
  ikp-ai-chat-help-actions__menu-item
  ikp-ai-chat-help-actions_menu-item-icon
  ikp-ai-chat-tagline__container
  ikp-ai-chat-tagline__text
  ikp-ai-chat-tagline__logo
  ikp-ai-chat-feedback-modal
  ikp-ai-chat-feedback-modal__overlay
  ikp-ai-chat-feedback-modal__content
  ikp-ai-chat-feedback-modal__header
  ikp-ai-chat-feedback-modal__close
  ikp-ai-chat-feedback-modal__body
  ikp-ai-chat-image-preview-modal
  ikp-ai-chat-image-preview-modal__overlay
  ikp-ai-chat-image-preview-modal__content
  ikp-ai-chat-image-preview-modal__image
  ikp-ai-chat-image-preview-modal__footer
  ikp-ai-chat-image-preview-modal__file-name
  ikp-ai-chat-image-preview-modal__close
  ikp-ai-chat-feedback-form
  ikp-ai-chat-feedback-form__field
  ikp-ai-chat-feedback-item__label
  ikp-ai-chat-feedback-item__description
  ikp-ai-chat-feedback-form__submit-button
  ikp-ai-chat-form__wrapper
  ikp-ai-chat-form
  ikp-ai-chat-form__close
  ikp-ai-chat-form__header
  ikp-ai-chat-form__heading
  ikp-ai-chat-form__description
  ikp-ai-chat-form__content
  ikp-ai-chat-form__field
  ikp-ai-chat-form__field-label
  ikp-ai-chat-form__field-text
  ikp-ai-chat-form__field-email
  ikp-ai-chat-form__field-file
  ikp-ai-chat-form__field-text-area
  ikp-ai-chat-form__field-checkbox
  ikp-ai-chat-form__field-checkbox-indicator
  ikp-ai-chat-form__field-select
  ikp-ai-chat-form__field-select__trigger
  ikp-ai-chat-form__field-select__value
  ikp-ai-chat-form__field-select__icon
  ikp-ai-chat-form__field-select__content
  ikp-ai-chat-form__field-select__viewport
  ikp-ai-chat-form__field-select__item
  ikp-ai-chat-form__field-select__item-indicator
  ikp-ai-chat-form__field-select__item-text
  ikp-ai-chat-form__field-combobox__control
  ikp-ai-chat-form__field-combobox__positioner
  ikp-ai-chat-form__field-combobox__input
  ikp-ai-chat-form__field-combobox__trigger
  ikp-ai-chat-form__field-combobox__selected-tags
  ikp-ai-chat-form__field-combobox__content
  ikp-ai-chat-form__field-combobox__list
  ikp-ai-chat-form__field-combobox__list-empty
  ikp-ai-chat-form__field-combobox__item
  ikp-ai-chat-form__field-combobox__item-text
  ikp-ai-chat-form__field-combobox__item-indicator
  ikp-ai-chat-form__field-description
  ikp-ai-chat-form__field-error
  ikp-ai-chat-form__error
  ikp-ai-chat-form__footer
  ikp-ai-chat-form__cancel
  ikp-ai-chat-form__submit
  ikp-ai-chat-form__success
  ikp-ai-chat-form__success-heading
  ikp-ai-chat-form__success-message
  ikp-ai-chat-form__success-button
  ikp-ai-chat-link
  ```

  ### Chat History Components

  ```css
  ikp-ai-chat-history
  ikp-ai-chat-history__header
  ikp-ai-chat-history__back-button
  ikp-ai-chat-history__back-button-icon
  ikp-ai-chat-history__title
  ikp-ai-chat-history__list
  ikp-ai-chat-history__item
  ikp-ai-chat-history__item-title
  ikp-ai-chat-history__item-date
  ikp-ai-chat-history__empty-state
  ikp-ai-chat-history__loading-state
  ikp-ai-chat-history__footer
  ikp-ai-chat-history__new-conversation-button
  ```

  ### Markdown Components

  ```css
  ikp-codeblock-container
  ikp-codeblock-header
  ikp-codeblock-header-language
  ikp-codeblock-copy-button
  ikp-codeblock-highlighter-wrapper
  ikp-codeblock-highlighter
  ikp-codeblock-code
  ikp-markdown-h1
  ikp-markdown-h2
  ikp-markdown-p
  ikp-markdown-li
  ikp-markdown-ul
  ikp-markdown-ol
  ikp-markdown-link
  ikp-markdown-source-link
  ikp-markdown-table
  ikp-markdown-th
  ikp-markdown-td
  ikp-markdown-code
  ikp-markdown-input
  ikp-markdown-sup
  ikp-markdown-img
  ikp-markdown-hr
  ```

  ### Search Bar Components

  ```css
  ikp-search-bar__container
  ikp-search-bar__button
  ikp-search-bar__content-wrapper
  ikp-search-bar__text
  ikp-search-bar__icon
  ikp-search-bar__kbd-wrapper
  ikp-search-bar__cmd-icon
  ikp-search-bar__ctrl
  ikp-search-bar__kbd-shortcut-key
  ```

  ### Modal Components

  ```css
  ikp-modal
  ikp-modal__overlay
  ikp-modal__content
  ikp-modal__close
  ```

  ### Chat Button Components

  ```css
  ikp-chat-button__text
  ikp-chat-button__button
  ikp-chat-button__container
  ikp-chat-button__avatar-image
  ikp-chat-button__avatar-content
  ikp-chat-button__close-icon
  ```

  ### Miscellaneous Components

  ```css
  ikp-icon
  ikp-loading-indicator__text
  ikp-loading-indicator__dots
  ikp-loading-indicator__dot
  ikp-view_toggle
  ikp-view_toggle_button
  ikp-view_toggle_icon
  ```
</>


# Chat Button for JavaScript (/talk-to-your-agents/javascript/chat-button)

Learn how to use the chat button component in JavaScript



The chat button component is a simple way to trigger the Inkeep chat bubble in your JavaScript application.

## Quick Start

<Steps>
  <Step>
    Add the below `<script>` tag to the `<head>` or `<body>` of your website.

    ```html
    <script
        type="module"
        src="https://cdn.jsdelivr.net/npm/@inkeep/agents-ui-js-cloud@0.17/dist/embed.js"
        defer
    ></script>
    ```
  </Step>

  <Step>
    Insert the Chat Button by using the `Inkeep.ChatButton()` function.

    ```ts
    const config: InkeepChatButtonProps = {
      aiChatSettings: {
        baseUrl: "https://your-api.example.com",
        appId: process.env.INKEEP_APP_ID, // Your app ID
      },
    };

    const chatButton = Inkeep.ChatButton(config);
    ```
  </Step>
</Steps>

## Config

<AutoTypeTable name="default" type="export { InkeepChatButtonProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsChatButton

<AutoTypeTable name="default" type="export { OpenSettingsChatButton as default } from '@inkeep/agents-ui/types'" />

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>


# Custom Trigger for JavaScript (/talk-to-your-agents/javascript/custom-trigger)

Create custom UI elements to trigger Inkeep modals with complete control over appearance and behavior in JavaScript.



## Overview

Custom triggers give you complete control over how users open Inkeep modals in your JavaScript application. Instead of using pre-built widgets like `Inkeep.ChatButton()`, you can create your own UI elements that match your design system while maintaining full functionality.

## Quick Start

<Steps>
  <Step>
    Add the below `<script>` tag to the `<head>` or `<body>` of your website.

    ```html
    <script
      type="module"
      src="https://cdn.jsdelivr.net/npm/@inkeep/agents-ui-js-cloud@0.17/dist/embed.js"
      defer
    ></script>
    ```
  </Step>

  <Step>
    Insert the ModalChat widget by using the `Inkeep.ModalChat()` function.

    ```js
    const config = {
      aiChatSettings: {
        baseUrl: "https://your-api.example.com",
        appId: process.env.INKEEP_APP_ID, // Your app ID
        aiAssistantName: "Keepie",
      },
      openSettings: {
        onOpenChange: handleOpenChange,
      },
    };

    // Initialize modal chat
    const modalChat = Inkeep.ModalChat(config);

    function handleOpenChange(newOpen) {
      modalChat.update({ openSettings: { isOpen: newOpen } });
    }

    // trigger
    document.querySelector("#chat-button").addEventListener("click", () => {
      modalChat.update({ openSettings: { isOpen: true } });
    });

    // Access chat methods
    modalChat.clearChat();
    ```
  </Step>
</Steps>

## Config

<AutoTypeTable name="default" type="export { InkeepModalChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsModal

<AutoTypeTable name="default" type="export { OpenSettingsModal as default } from '@inkeep/agents-ui/types'" />


# Embedded Chat for JavaScript (/talk-to-your-agents/javascript/embedded-chat)

Integrate Inkeep's embedded chat component directly into your JavaScript application for inline agent conversations.



The EmbeddedChat widget provides a fully embedded chat interface that you can integrate directly into your JavaScript application's layout. Unlike modal or sidebar implementations, the embedded chat becomes part of your page structure, perfect for dedicated support pages, dashboards, or help centers.

## Quick Start

<Steps>
  <Step>
    Add the below `<script>` tag to the `<head>` or `<body>` of your website.

    ```html
    <script
        type="module"
        src="https://cdn.jsdelivr.net/npm/@inkeep/agents-ui-js-cloud@0.17/dist/embed.js"
        defer

    > </script>

    ```
  </Step>

  <Step>
    Define an element in your page that will be the "container" for the embedded chat.

    ```html
    <div style="display: flex; align-items: center; justify-content: center; height: calc(100vh - 16px);">
      <div style="max-height: 600px; height: 100%;">
        <div id="ikp-embedded-chat-target"></div>
      </div>
    </div>
    ```
  </Step>

  <Step>
    Insert the EmbeddedChat widget by using the `Inkeep.EmbeddedChat()` function.

    ```ts
    <script type="module">
      const config = {
        aiChatSettings: {
          baseUrl: "https://your-api.example.com",
          appId: process.env.INKEEP_APP_ID, // Your app ID
        },
      };

      // Initialize the widget
      const widget = Inkeep.EmbeddedChat("#ikp-embedded-chat-target", config);
    </script>
    ```
  </Step>
</Steps>

## Config

<AutoTypeTable name="default" type="export { InkeepEmbeddedChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>


# Add Sidebar Chat with JavaScript (/talk-to-your-agents/javascript/side-bar-chat)

Integrate Inkeep's sidebar chat component into your JavaScript application for seamless agent conversations.



The SidebarChat widget provides a slide-out panel interface for conversing with your agents. It can be positioned on either side of your application and supports both manual triggering and automatic element triggers.

## Quick Start

<Steps>
  <Step>
    Add the below `<script>` tag to the `<head>` or `<body>` of your website.

    ```html
    <script
        type="module"
        src="https://cdn.jsdelivr.net/npm/@inkeep/agents-ui-js-cloud@0.17/dist/embed.js"
        defer

    > </script>

    ```
  </Step>

  <Step>
    Define an element in your page that will be the "container" for the embedded chat.

    ```html
    <div style="display: flex; flex-direction: row; height: 100vh; max-height: 100vh; padding: 0; margin: 0; overflow: hidden;">
      <div style="display: flex; flex-direction: column; height: 100vh; max-height: 100vh; padding: 0; margin: 0; overflow-y: auto; flex: 1;">
        <!-- your app content here -->
      </div>
      <!-- the sidebar chat will be inserted into this div -->
      <div id="sidebar-chat"></div>
    </div>
    ```
  </Step>

  <Step>
    Add a button element that will trigger the sidebar chat and give it the data attribute data-inkeep-sidebar-chat-trigger. You can also use your own custom attribute if you prefer, just be sure to pass it to the triggerSelector prop.

    ```html
    <div style="display: flex; flex-direction: row; height: 100vh; max-height: 100vh; padding: 0; margin: 0; overflow: hidden;">
      <div style="display: flex; flex-direction: column; height: 100vh; max-height: 100vh; padding: 0; margin: 0; overflow-y: auto; flex: 1;">
        <!-- your app content here -->
        <button data-inkeep-sidebar-chat-trigger="">Toggle Sidebar Chat</button>
      </div>
      <!-- the sidebar chat will be inserted into this div -->
      <div id="ikp-sidebar-chat-target"></div>
    </div>
    ```
  </Step>

  <Step>
    Insert the EmbeddedChat widget by using the `Inkeep.EmbeddedChat()` function.

    ```ts
    <script type="module">
      const config = {
        aiChatSettings: {
          baseUrl: "https://your-api.example.com",
          appId: process.env.INKEEP_APP_ID, // Your app ID
        },
      };

      // Initialize the widget
      const widget = Inkeep.SidebarChat("#ikp-sidebar-chat-target", config);
    </script>
    ```
  </Step>
</Steps>

## Config

<AutoTypeTable name="default" type="export { InkeepEmbeddedChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsSidebar

<AutoTypeTable name="default" type="export { OpenSettingsSidebar as default } from '@inkeep/agents-ui/types'" />


# Chat Button for Next.js (/talk-to-your-agents/nextjs/chat-button)

Use the chat button component in Next.js with App Router and dynamic import.



<Note>
  For Next.js apps that use App Router, use the `"use client"` directive at the
  top of the file to load the widget client-side.
</Note>

The chat button component is a simple way to trigger the Inkeep chat bubble. In Next.js, load it with `next/dynamic` and `ssr: false`.

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Quick Start

<Tabs>
  <Tab title="Production">
    Use an API key for secure authentication in production:

    ```tsx
    'use client';

    import dynamic from 'next/dynamic';
    import type { InkeepChatButtonProps } from '@inkeep/agents-ui-cloud';

    const InkeepChatButton = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepChatButton),
      { ssr: false },
    );

    const buttonProps: InkeepChatButtonProps = {
      aiChatSettings: {
        baseUrl: 'https://your-api.example.com',
        appId: process.env.NEXT_PUBLIC_INKEEP_APP_ID, // Your app ID
      },
    };

    export default function ChatButton() {
      return <InkeepChatButton {...buttonProps} />;
    }
    ```

    **To get an API key:**

    1. Open the Visual Builder Dashboard
    2. Navigate to your Project → **API Keys**
    3. Click **Create New Key** and select your agent
    4. Copy the key (shown once) and store it in your environment variables
  </Tab>

  <Tab title="Development">
    For local development, use headers with your agent IDs:

    ```tsx
    'use client';

    import dynamic from 'next/dynamic';
    import type { InkeepChatButtonProps } from '@inkeep/agents-ui-cloud';

    const InkeepChatButton = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepChatButton),
      { ssr: false },
    );

    const buttonProps: InkeepChatButtonProps = {
      aiChatSettings: {
        baseUrl: 'http://localhost:3003',
        appId: 'YOUR_APP_ID',
      },
    };

    export default function ChatButton() {
      return <InkeepChatButton {...buttonProps} />;
    }
    ```

    **Note:** Run your API server with `ENVIRONMENT=development` to bypass authentication.
  </Tab>
</Tabs>

## Props

<AutoTypeTable name="default" type="export { InkeepChatButtonProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsChatButton

<AutoTypeTable name="default" type="export { OpenSettingsChatButton as default } from '@inkeep/agents-ui/types'" />

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>


# Custom Trigger for Next.js (/talk-to-your-agents/nextjs/custom-trigger)

Create custom UI elements to trigger Inkeep modals in Next.js with App Router and dynamic import.



<Note>
  For Next.js apps that use App Router, use the `"use client"` directive at the
  top of the file to load the widget client-side.
</Note>

Custom triggers give you complete control over how users open Inkeep modals in your Next.js application. Load `InkeepModalChat` with `next/dynamic` and `ssr: false`.

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Quick Start

<Tabs>
  <Tab title="Production">
    Use an API key for secure authentication in production:

    ```tsx
    'use client';

    import { useState } from 'react';
    import dynamic from 'next/dynamic';
    import type { InkeepModalChatProps } from '@inkeep/agents-ui-cloud';

    const InkeepModalChat = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepModalChat),
      { ssr: false },
    );

    export default function CustomTrigger() {
      const [isModalOpen, setIsModalOpen] = useState(false);

      const modalProps: InkeepModalChatProps = {
        aiChatSettings: {
          baseUrl: "https://your-api.example.com",
          appId: process.env.NEXT_PUBLIC_INKEEP_APP_ID, // Your app ID
        },
        openSettings: {
          isOpen: isModalOpen,
          onOpenChange: setIsModalOpen,
        },
      };

      return (
        <>
          <button onClick={() => setIsModalOpen(true)}>
            Open Inkeep Assistant
          </button>
          <InkeepModalChat {...modalProps} />
        </>
      );
    }
    ```

    **To get an API key:**

    1. Open the Visual Builder Dashboard
    2. Navigate to your Project → **API Keys**
    3. Click **Create New Key** and select your agent
    4. Copy the key (shown once) and store it in your environment variables
  </Tab>

  <Tab title="Development">
    For local development, use headers with your agent IDs:

    ```tsx
    'use client';

    import { useState } from 'react';
    import dynamic from 'next/dynamic';
    import type { InkeepModalChatProps } from '@inkeep/agents-ui-cloud';

    const InkeepModalChat = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepModalChat),
      { ssr: false },
    );

    export default function CustomTrigger() {
      const [isModalOpen, setIsModalOpen] = useState(false);

      const modalProps: InkeepModalChatProps = {
        aiChatSettings: {
          baseUrl: "http://localhost:3003",
          appId: process.env.NEXT_PUBLIC_INKEEP_APP_ID, // Your app ID
        },
        openSettings: {
          isOpen: isModalOpen,
          onOpenChange: setIsModalOpen,
        },
      };

      return (
        <>
          <button onClick={() => setIsModalOpen(true)}>
            Open Inkeep Assistant
          </button>
          <InkeepModalChat {...modalProps} />
        </>
      );
    }
    ```

    **Note:** Run your API server with `ENVIRONMENT=development` to bypass authentication.
  </Tab>
</Tabs>

## Props

<AutoTypeTable name="default" type="export { InkeepModalChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsModal

<AutoTypeTable name="default" type="export { OpenSettingsModal as default } from '@inkeep/agents-ui/types'" />


# Embedded Chat for Next.js (/talk-to-your-agents/nextjs/embedded-chat)

Integrate Inkeep's embedded chat component in Next.js with App Router and dynamic import.



<Note>
  For Next.js apps that use App Router, use the `"use client"` directive at the
  top of the file to load the widget client-side.
</Note>

The InkeepEmbeddedChat component provides a fully embedded chat interface. In Next.js, load it with `next/dynamic` and `ssr: false`. It fits dedicated support pages, dashboards, or help centers.

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Quick Start

<Tabs>
  <Tab title="Production">
    Use an API key for secure authentication in production:

    ```tsx
    'use client';

    import dynamic from 'next/dynamic';
    import type { InkeepEmbeddedChatProps } from '@inkeep/agents-ui-cloud';

    const InkeepEmbeddedChat = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepEmbeddedChat),
      { ssr: false },
    );

    const embeddedChatProps: InkeepEmbeddedChatProps = {
      aiChatSettings: {
        baseUrl: "https://your-api.example.com",
        appId: process.env.NEXT_PUBLIC_INKEEP_APP_ID, // Your app ID
      },
    };

    export default function EmbeddedChatPage() {
      return <InkeepEmbeddedChat {...embeddedChatProps} />;
    }
    ```

    **To get an API key:**

    1. Open the Visual Builder Dashboard
    2. Navigate to your Project → **API Keys**
    3. Click **Create New Key** and select your agent
    4. Copy the key (shown once) and store it in your environment variables
  </Tab>

  <Tab title="Development">
    For local development, use headers with your agent IDs:

    ```tsx
    'use client';

    import dynamic from 'next/dynamic';
    import type { InkeepEmbeddedChatProps } from '@inkeep/agents-ui-cloud';

    const InkeepEmbeddedChat = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepEmbeddedChat),
      { ssr: false },
    );

    const embeddedChatProps: InkeepEmbeddedChatProps = {
      aiChatSettings: {
        baseUrl: 'http://localhost:3003',
        appId: 'YOUR_APP_ID',
      },
    };

    export default function EmbeddedChatPage() {
      return <InkeepEmbeddedChat {...embeddedChatProps} />;
    }
    ```

    **Note:** Run your API server with `ENVIRONMENT=development` to bypass authentication.
  </Tab>
</Tabs>

## Props

<AutoTypeTable name="default" type="export { InkeepEmbeddedChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>


# Add Sidebar Chat to Next.js (/talk-to-your-agents/nextjs/side-bar-chat)

Integrate Inkeep's sidebar chat component in Next.js with App Router and dynamic import.



<Note>
  For Next.js apps that use App Router, use the `"use client"` directive at the
  top of the file to load the widget client-side.
</Note>

The InkeepSidebarChat component provides a slide-out panel for conversing with your agents. In Next.js, load it with `next/dynamic` and `ssr: false`. It can be positioned on either side and supports state or data-attribute triggers.

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Quick Start

<Tabs>
  <Tab title="Production">
    Use an API key for secure authentication in production:

    ### Trigger through state

    ```tsx
    'use client';

    import { useState } from 'react';
    import dynamic from 'next/dynamic';
    import type { InkeepSidebarChatProps } from '@inkeep/agents-ui-cloud';

    const InkeepSidebarChat = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepSidebarChat),
      { ssr: false },
    );

    export default function SidebarChatPage() {
      const [isOpen, setIsOpen] = useState(false);

      const sidebarChatProps: InkeepSidebarChatProps = {
        aiChatSettings: {
          baseUrl: "https://your-api.example.com",
          appId: process.env.NEXT_PUBLIC_INKEEP_APP_ID, // Your app ID
        },
        position: 'right',
        openSettings: {
          isOpen,
          onOpenChange: setIsOpen,
        },
      };

      return (
        <div>
          <button onClick={() => setIsOpen(!isOpen)}>
            {isOpen ? 'Close' : 'Open'} Sidebar Chat
          </button>
          <InkeepSidebarChat {...sidebarChatProps} />
        </div>
      );
    }
    ```

    ### Trigger through data attributes

    ```tsx
    'use client';

    import dynamic from 'next/dynamic';
    import type { InkeepSidebarChatProps } from '@inkeep/agents-ui-cloud';

    const InkeepSidebarChat = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepSidebarChat),
      { ssr: false },
    );

    const sidebarChatProps: InkeepSidebarChatProps = {
      aiChatSettings: {
        baseUrl: "https://your-api.example.com",
        appId: process.env.NEXT_PUBLIC_INKEEP_APP_ID, // Your app ID
      },
      position: 'right',
    };

    export default function SidebarChatPage() {
      return (
        <>
          <button data-inkeep-sidebar-chat-trigger>
            Click here to open sidebar chat
          </button>
          <InkeepSidebarChat {...sidebarChatProps} />
        </>
      );
    }
    ```

    **To get an API key:**

    1. Open the Visual Builder Dashboard
    2. Navigate to your Project → **API Keys**
    3. Click **Create New Key** and select your agent
    4. Copy the key (shown once) and store it in your environment variables
  </Tab>

  <Tab title="Development">
    For local development, use headers with your agent IDs:

    ### Trigger through state

    ```tsx
    'use client';

    import { useState } from 'react';
    import dynamic from 'next/dynamic';
    import type { InkeepSidebarChatProps } from '@inkeep/agents-ui-cloud';

    const InkeepSidebarChat = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepSidebarChat),
      { ssr: false },
    );

    export default function SidebarChatPage() {
      const [isOpen, setIsOpen] = useState(false);

      const sidebarChatProps: InkeepSidebarChatProps = {
        aiChatSettings: {
          baseUrl: 'http://localhost:3003',
          appId: 'YOUR_APP_ID',
        },
        position: 'right',
        openSettings: {
          isOpen,
          onOpenChange: setIsOpen,
        },
      };

      return (
        <div>
          <button onClick={() => setIsOpen(!isOpen)}>
            {isOpen ? 'Close' : 'Open'} Sidebar Chat
          </button>
          <InkeepSidebarChat {...sidebarChatProps} />
        </div>
      );
    }
    ```

    ### Trigger through data attributes

    ```tsx
    'use client';

    import dynamic from 'next/dynamic';
    import type { InkeepSidebarChatProps } from '@inkeep/agents-ui-cloud';

    const InkeepSidebarChat = dynamic(
      () => import('@inkeep/agents-ui-cloud').then((mod) => mod.InkeepSidebarChat),
      { ssr: false },
    );

    const sidebarChatProps: InkeepSidebarChatProps = {
      aiChatSettings: {
        baseUrl: 'http://localhost:3003',
        appId: 'YOUR_APP_ID',
      },
      position: 'right',
    };

    export default function SidebarChatPage() {
      return (
        <>
          <button data-inkeep-sidebar-chat-trigger>
            Click here to open sidebar chat
          </button>
          <InkeepSidebarChat {...sidebarChatProps} />
        </>
      );
    }
    ```

    **Note:** Run your API server with `ENVIRONMENT=development` to bypass authentication.
  </Tab>
</Tabs>

## Configuration Options

### Required Props

| Property                 | Type   | Description                              |
| ------------------------ | ------ | ---------------------------------------- |
| `aiChatSettings`         | object | Configuration for the AI chat connection |
| `aiChatSettings.baseUrl` | string | The base URL of your API server          |
| `aiChatSettings.appId`   | string | The app ID of your project               |

## Props

<AutoTypeTable name="default" type="export { InkeepSidebarChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsSidebar

<AutoTypeTable name="default" type="export { OpenSettingsSidebar as default } from '@inkeep/agents-ui/types'" />


# Chat Button (/talk-to-your-agents/react/chat-button)

Learn how to use the chat button component in React



The chat button component is a simple way to trigger the Inkeep chat bubble.

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Quick Start

<Tabs>
  <Tab title="Production">
    Use an API key for secure authentication in production:

    ```tsx
    import { InkeepChatButton, type InkeepChatButtonProps } from "@inkeep/agents-ui-cloud";

    const buttonProps: InkeepChatButtonProps = {
      aiChatSettings: {
        baseUrl: "https://your-api.example.com",
        appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
      },
    };

    const ChatButton = () => {
      return <InkeepChatButton {...buttonProps} />;
    };
    ```

    **To get an API key:**

    1. Open the Visual Builder Dashboard
    2. Navigate to your Project → **API Keys**
    3. Click **Create New Key** and select your agent
    4. Copy the key (shown once) and store it in your environment variables
  </Tab>

  <Tab title="Development">
    For local development, use headers with your agent IDs:

    ```tsx
    import {
      InkeepChatButton,
      type InkeepChatButtonProps,
    } from "@inkeep/agents-ui-cloud";

    const buttonProps: InkeepChatButtonProps = {
      aiChatSettings: {
        baseUrl: "http://localhost:3003",
        appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
      },
    };

    const ChatButton = () => {
      return <InkeepChatButton {...buttonProps} />;
    };
    ```

    **Note:** Run your API server with `ENVIRONMENT=development` to bypass authentication.
  </Tab>
</Tabs>

## Props

<AutoTypeTable name="default" type="export { InkeepChatButtonProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsChatButton

<AutoTypeTable name="default" type="export { OpenSettingsChatButton as default } from '@inkeep/agents-ui/types'" />

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>


# Custom Trigger (/talk-to-your-agents/react/custom-trigger)

Create custom UI elements to trigger Inkeep modals with complete control over appearance and behavior.



## Overview

Custom triggers give you complete control over how users open Inkeep modals in your React application. Instead of using pre-built components like `InkeepChatButton`, you can create your own UI elements that match your design system while maintaining full functionality.

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Quick Start

<Tabs>
  <Tab title="Production">
    Use an API key for secure authentication in production:

    ```tsx
    import { useState } from "react";
    import { InkeepModalChat, type InkeepModalChatProps } from "@inkeep/agents-ui-cloud";

    function App() {
      const [isModalOpen, setIsModalOpen] = useState(false);

      const modalProps: InkeepModalChatProps = {
        aiChatSettings: {
          baseUrl: "https://your-api.example.com",
          appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
        },
        openSettings: {
          isOpen: isModalOpen,
          onOpenChange: setIsModalOpen,
        },
      };

      return (
        <>
          <button onClick={() => setIsModalOpen(true)}>
            Open Inkeep Assistant
          </button>

          <InkeepModalChat {...modalProps} />
        </>
      );
    }
    ```

    **To get an API key:**

    1. Open the Visual Builder Dashboard
    2. Navigate to your Project → **API Keys**
    3. Click **Create New Key** and select your agent
    4. Copy the key (shown once) and store it in your environment variables
  </Tab>

  <Tab title="Development">
    For local development, use headers with your agent IDs:

    ```tsx
    import { useState } from "react";
    import {
      InkeepModalChat,
      type InkeepModalChatProps,
    } from "@inkeep/agents-ui-cloud";

    function App() {
      const [isModalOpen, setIsModalOpen] = useState(false);

      const modalProps: InkeepModalChatProps = {
        aiChatSettings: {
          baseUrl: "http://localhost:3003",
          appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
        },
        openSettings: {
          isOpen: isModalOpen,
          onOpenChange: setIsModalOpen,
        },
      };

      return (
        <>
          <button onClick={() => setIsModalOpen(true)}>
            Open Inkeep Assistant
          </button>

          <InkeepModalChat {...modalProps} />
        </>
      );
    }
    ```

    **Note:** Run your API server with `ENVIRONMENT=development` to bypass authentication.
  </Tab>
</Tabs>

## Props

<AutoTypeTable name="default" type="export { InkeepModalChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsModal

<AutoTypeTable name="default" type="export { OpenSettingsModal as default } from '@inkeep/agents-ui/types'" />


# Embedded Chat (/talk-to-your-agents/react/embedded-chat)

Integrate Inkeep's embedded chat component directly into your React application for inline agent conversations.



The InkeepEmbeddedChat component provides a fully embedded chat interface that you can integrate directly into your React application's layout. Unlike modal or sidebar implementations, the embedded chat becomes part of your page structure, perfect for dedicated support pages, dashboards, or help centers.

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Quick Start

<Tabs>
  <Tab title="Production">
    Use an API key for secure authentication in production:

    ```tsx
    import { InkeepEmbeddedChat, type InkeepEmbeddedChatProps } from "@inkeep/agents-ui-cloud";

    export default function App() {
      const embeddedChatProps: InkeepEmbeddedChatProps = {
        aiChatSettings: {
          baseUrl: "https://your-api.example.com",
          appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
        },
      };

      return <InkeepEmbeddedChat {...embeddedChatProps} />;
    }
    ```

    **To get an API key:**

    1. Open the Visual Builder Dashboard
    2. Navigate to your Project → **API Keys**
    3. Click **Create New Key** and select your agent
    4. Copy the key (shown once) and store it in your environment variables
  </Tab>

  <Tab title="Development">
    For local development, use headers with your agent IDs:

    ```tsx
    import {
      InkeepEmbeddedChat,
      type InkeepEmbeddedChatProps,
    } from "@inkeep/agents-ui-cloud";

    export default function App() {
      const embeddedChatProps: InkeepEmbeddedChatProps = {
        aiChatSettings: {
          baseUrl: "http://localhost:3003",
          appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
        },
      };

      return <InkeepEmbeddedChat {...embeddedChatProps} />;
    }
    ```

    **Note:** Run your API server with `ENVIRONMENT=development` to bypass authentication.
  </Tab>
</Tabs>

## Props

<AutoTypeTable name="default" type="export { InkeepEmbeddedChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>


# Add Sidebar Chat to React (/talk-to-your-agents/react/side-bar-chat)

Integrate Inkeep's sidebar chat component into your React application for seamless agent conversations.



The InkeepSidebarChat component provides a slide-out panel interface for conversing with your agents. It can be positioned on either side of your application and supports both manual triggering and automatic element triggers.

## Installation

<CodeGroup>
  ```bash title="npm"
  npm install @inkeep/agents-ui-cloud
  ```

  ```bash title="yarn"
  yarn add @inkeep/agents-ui-cloud
  ```

  ```bash title="pnpm"
  pnpm add @inkeep/agents-ui-cloud
  ```
</CodeGroup>

## Quick Start

<Tabs>
  <Tab title="Production">
    Use an API key for secure authentication in production:

    ### Trigger through React state

    ```tsx
    import { useState } from "react";
    import { InkeepSidebarChat, type InkeepSidebarChatProps } from "@inkeep/agents-ui-cloud";

    export default function App() {
      const [isOpen, setIsOpen] = useState(false);

      const sidebarChatProps: InkeepSidebarChatProps = {
        aiChatSettings: {
          baseUrl: "https://your-api.example.com",
          appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
        },
        position: "right",
        openSettings: {
          isOpen: isOpen,
          onOpenChange: setIsOpen,
        },
      };

      return (
        <div>
          <button onClick={() => setIsOpen(!isOpen)}>
            {isOpen ? "Close" : "Open"} Sidebar Chat
          </button>

          <InkeepSidebarChat {...sidebarChatProps} />
        </div>
      );
    }
    ```

    ### Trigger through data attributes

    ```tsx
    import {
      InkeepSidebarChat,
      type InkeepSidebarChatProps,
    } from "@inkeep/agents-ui-cloud";

    export default function App() {
      const sidebarChatProps: InkeepSidebarChatProps = {
        aiChatSettings: {
          baseUrl: "https://your-api.example.com",
          appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
        },
        position: "right",
      };

      return (
        <div>
          <button data-inkeep-sidebar-chat-trigger>
            Click here to open sidebar chat
          </button>

          <InkeepSidebarChat {...sidebarChatProps} />
        </div>
      );
    }
    ```

    **To get an API key:**

    1. Open the Visual Builder Dashboard
    2. Navigate to your Project → **API Keys**
    3. Click **Create New Key** and select your agent
    4. Copy the key (shown once) and store it in your environment variables
  </Tab>

  <Tab title="Development">
    For local development, use headers with your agent IDs:

    ### Trigger through React state

    ```tsx
    import { useState } from "react";
    import {
      InkeepSidebarChat,
      type InkeepSidebarChatProps,
    } from "@inkeep/agents-ui-cloud";

    export default function App() {
      const [isOpen, setIsOpen] = useState(false);

      const sidebarChatProps: InkeepSidebarChatProps = {
        aiChatSettings: {
          baseUrl: "http://localhost:3003",
          appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
        },
        position: "right",
        openSettings: {
          isOpen: isOpen,
          onOpenChange: setIsOpen,
        },
      };

      return (
        <div>
          <button onClick={() => setIsOpen(!isOpen)}>
            {isOpen ? "Close" : "Open"} Sidebar Chat
          </button>

          <InkeepSidebarChat {...sidebarChatProps} />
        </div>
      );
    }
    ```

    ### Trigger through data attributes

    ```tsx
    import {
      InkeepSidebarChat,
      type InkeepSidebarChatProps,
    } from "@inkeep/agents-ui-cloud";

    export default function App() {
      const sidebarChatProps: InkeepSidebarChatProps = {
        aiChatSettings: {
          baseUrl: "http://localhost:3003",
          appId: process.env.REACT_APP_INKEEP_APP_ID, // Your app ID
        },
        position: "right",
      };

      return (
        <div>
          <button data-inkeep-sidebar-chat-trigger>
            Click here to open sidebar chat
          </button>

          <InkeepSidebarChat {...sidebarChatProps} />
        </div>
      );
    }
    ```

    **Note:** Run your API server with `ENVIRONMENT=development` to bypass authentication.
  </Tab>
</Tabs>

## Configuration Options

### Required Props

| Property                 | Type   | Description                              |
| ------------------------ | ------ | ---------------------------------------- |
| `aiChatSettings`         | object | Configuration for the AI chat connection |
| `aiChatSettings.baseUrl` | string | The base URL of your API server          |
| `aiChatSettings.appId`   | string | The app ID of your project               |

## Props

<AutoTypeTable name="default" type="export { InkeepSidebarChatProps as default } from '@inkeep/agents-ui'" />

<>
  ### InkeepBaseSettings

  <AutoTypeTable name="default" type="export { InkeepBaseSettings as default } from '@inkeep/agents-ui/types'" />

  #### ColorModeConfig

  <AutoTypeTable name="default" type="export { ColorModeConfig as default } from '@inkeep/agents-ui/types'" />

  #### UserProperties

  <AutoTypeTable name="default" type="export { UserProperties as default } from '@inkeep/agents-ui/types'" />
</>

<>
  ### InkeepAIChatSettings

  <AutoTypeTable name="default" type="export { InkeepAIChatSettings as default } from '@inkeep/agents-ui/types'" />

  #### AIChatFunctions

  <AutoTypeTable name="default" type="export { AIChatFunctions as default } from '@inkeep/agents-ui/types'" />

  #### AIChatDisclaimerSettings

  <AutoTypeTable name="default" type="export { AIChatDisclaimerSettings as default } from '@inkeep/agents-ui/types'" />

  #### GetHelpOption

  <AutoTypeTable name="default" type="export { GetHelpOption as default } from '@inkeep/agents-ui/types'" />

  #### CustomMessageAction

  <AutoTypeTable name="default" type="export { CustomMessageAction as default } from '@inkeep/agents-ui/types'" />

  #### AIChatToolbarButtonLabels

  <AutoTypeTable name="default" type="export { AIChatToolbarButtonLabels as default } from '@inkeep/agents-ui/types'" />

  #### SearchAndChatFilters

  <AutoTypeTable name="default" type="export { SearchAndChatFilters as default } from '@inkeep/agents-ui/types'" />

  #### ComponentsConfig

  <AutoTypeTable name="default" type="export { ComponentsConfig as default } from '@inkeep/agents-ui/types'" />
</>

### OpenSettingsSidebar

<AutoTypeTable name="default" type="export { OpenSettingsSidebar as default } from '@inkeep/agents-ui/types'" />


# Trigger Inkeep agents from Zendesk tickets (/visual-builder/triggers/ticketing/zendesk)

Wire Zendesk to POST tickets to an Inkeep webhook trigger so an agent can act on them.



When something happens to a Zendesk ticket — a new one is created, an existing one is solved, a tag changes — you want Inkeep to know about it within seconds so an agent can act.

Zendesk offers **two ways to fire a webhook**, and each maps cleanly to a different use case:

| Approach                  | When to use                                                                                                                          | What you set up                                          |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
| **Zendesk events**        | You want the simplest setup and just need to fire on a standard event like *Ticket created* or *Ticket updated*. No filtering.       | Just the webhook — subscribe it to one or more events.   |
| **Trigger or automation** | You need to filter which tickets fire (by tag, queue, form, channel, etc.) or you want to control the exact JSON body Zendesk sends. | A webhook **and** a business rule trigger that fires it. |

Below are both paths. Pick the one that matches your need.

## Prerequisites

* An Inkeep agent + webhook trigger already created. If you haven't done this yet, follow the [Webhook Triggers guide](/visual-builder/triggers/webhooks) — you'll need the **webhook URL** from the trigger before continuing.
* **Zendesk admin access** — you'll be making changes in the Zendesk Admin Center.

## Option 1: Subscribe to a Zendesk event

This is the leanest setup. Zendesk fires the webhook directly when the event happens — no business rule needed.

<Note>
  Following the [Ticket Workflows](/guides/agents/ticket-workflows/routing) tutorials? Use **Option 2** below instead. Event subscriptions send Zendesk's fixed native payload — the ticket id arrives as `detail.id`, there's no `ticket_id` field, and you can't customize the body. The tutorials expect the custom JSON body with an explicit `ticket_id`, which only Option 2 lets you define.
</Note>

<Steps>
  <Step>
    In Zendesk, open **Admin Center** (gear icon → Admin Center) and go to **Apps and integrations → Webhooks**.
  </Step>

  <Step>
    Click **Create webhook**.
  </Step>

  <Step>
    For the connection type, choose **Zendesk events**. Add **Ticket created** (or whichever event you want to listen for — `Ticket updated`, `Ticket solved`, etc.) and click **Next**.
  </Step>

  <Step>
    Fill in the webhook details:

    * **Name**: e.g. `Inkeep ticket-created webhook`
    * **Endpoint URL**: paste the webhook URL from your Inkeep trigger
    * **Request method**: `POST`
    * **Request format**: `JSON`
    * **Authentication**: `None`
  </Step>

  <Step>
    Click **Create webhook**.
  </Step>
</Steps>

## Option 2: Use a business rule trigger

Use this path when you need to filter, send a custom JSON body, or fire on conditions that aren't standard Zendesk events.

### Create the webhook

<Steps>
  <Step>
    In Zendesk, open **Admin Center** and go to **Apps and integrations → Webhooks**.
  </Step>

  <Step>
    Click **Create webhook**.
  </Step>

  <Step>
    For the connection type, choose **Trigger or automation** (not *Zendesk events*). Click **Next**.
  </Step>

  <Step>
    Fill in the webhook details:

    * **Name**: e.g. `Inkeep new-ticket webhook`
    * **Endpoint URL**: paste the webhook URL from your Inkeep trigger
    * **Request method**: `POST`
    * **Request format**: `JSON`
    * **Authentication**: `None`
  </Step>

  <Step>
    Click **Create webhook**.
  </Step>
</Steps>

### Create the trigger that fires it

<Steps>
  <Step>
    In Admin Center, go to **Objects and rules → Business rules → Triggers**, then click **Add trigger**.
  </Step>

  <Step>
    Give it a name (e.g. `New ticket → Inkeep`) and pick a category for organization.
  </Step>

  <Step>
    Under **Conditions → Meet ALL of the following**, add:

    * **Ticket → Ticket is** = **Created**
  </Step>

  <Step>
    Under **Actions**, add:

    * **Notifications → Notify active webhook**
    * In the dropdown, select the webhook you created above
    * In the **JSON body** field, paste the body below
  </Step>

  <Step>
    Click **Create**.
  </Step>
</Steps>

**JSON body:**

```json
{
  "ticket_id": "{{ticket.id}}",
  "subject": "{{ticket.title}}",
  "description": "{{ticket.description}}",
  "requester_name": "{{ticket.requester.name}}",
  "requester_email": "{{ticket.requester.email}}",
  "status": "{{ticket.status}}"
}
```

### Filter which tickets fire the webhook

Edit the trigger's **Conditions** to scope which tickets fire. The most common adjustments:

* **Fire on a different event** — change `Ticket is Created` to `Ticket is Updated`, `Solved`, etc.
* **Scope to a subset** — add conditions for tags, ticket form, channel, group, and more.

For the full list of conditions Zendesk supports, see [Zendesk's trigger conditions reference](https://support.zendesk.com/hc/en-us/articles/4408822236058).

## Next steps

<Cards>
  <Card title="Routing agent" icon="LuTags" href="/guides/agents/ticket-workflows/routing">
    Auto-tag every incoming ticket with the team that owns the problem.
  </Card>

  <Card title="Internal-note draft agent" icon="LuMessageSquareDot" href="/guides/agents/ticket-workflows/internal-note-draft">
    Draft a KB-grounded reply as a private comment for reps to review.
  </Card>

  <Card title="Auto-reply agent" icon="LuShieldCheck" href="/guides/agents/ticket-workflows/auto-reply">
    Reply publicly when the KB is confident — stay quiet when it isn't.
  </Card>

  <Card title="Webhook Triggers reference" icon="LuWebhook" href="/visual-builder/triggers/webhooks">
    Full reference for the Inkeep-side trigger configuration.
  </Card>
</Cards>