# Documentation (/)
Welcome to Agent Serve, the AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Chat, or programmatically with the API — connected to your business tools and the major LLM providers.
## Quick Start [#quick-start]
Learn what you can build with Agent Serve
Build your first agent in 10 minutes
Learn about the building blocks
Explore the integration catalog
## Work alongside AI [#work-alongside-ai]
AI teammates that take jobs and hand back finished work
Docs and whiteboards your team writes together, with Chat beside you
Add Chat commands, menu actions, and background work in one install
Charts, KPIs, reports, and governed sharing
Score call transcripts against your checklist
Data sources, metrics, semantic model, rules, and monitors
Query your data sources in SQL or plain English
## Build on Agent Serve [#build-on-agent-serve]
Install and open the apps your team runs inside the workspace
Build web apps that run inside the workspace
Build, deploy, and manage from the terminal
Reach tables, workflows, Relays, and Chat from an app
Build a version on every push from GitHub or GitLab
## Core Concepts [#core-concepts]
Understand how data flows between blocks
Work with workflow and environment variables
Inspect workflow runs and diagnose failures
Start agents via API, webhooks, or schedules
## Advanced Features [#advanced-features]
Set up workspace roles and permissions
Connect external services with Model Context Protocol
Integrate Agent Serve into your applications
---
# Choosing what to use (/agents/choosing)
When you build an agent, several features overlap: a deterministic block and an agent tool can run the same integration, and a custom tool, an MCP tool, and a workflow-as-tool can all give an agent the same action. This page lays out the differences so you pick the right one. They vary along three lines: whether the action is **deterministic** (always runs) or **model-decided** (an agent chooses), whether it lives in one workflow or is **reusable** across your workspace, and whether it comes from Agent Serve or an **external** provider.
The running example is a workflow that scores inbound sales leads. It reads a new lead, enriches it, decides on a score, logs the result, and notifies the team. Each option below builds part of it:
{/* VISUAL: decision tree. Always happens? → deterministic block. Else agent chooses? → agent tool. Reuse across workspace? → custom tool. External toolset? → MCP. Whole workflow? → workflow-as-tool. Reusable instructions? → skill. */}
## Deterministic block [#deterministic-block]
A **block** is a single step that runs at a fixed point on the path, with no model deciding whether to. It always runs when the workflow reaches it. Use one when the action must happen every time: an API call, a data transform, a branch.
In the lead scorer, a [Google Sheets](/integrations) block always appends the scored lead to a tracking sheet, and a [Function](/workflows/blocks/function) block always reshapes the enrichment response into the fields the next step expects. The block runs at that point in the graph; an execution error can still stop it.
Most steps in a workflow are blocks. Reach for the kinds below only when you want a model to decide, or you want to reuse something.
## Agent tool [#agent-tool]
An **agent tool** is an action you hand to an [Agent](/workflows/blocks/agent) block. The agent reads the task and decides whether and when to call it. The same catalog of [integrations](/integrations) that exist as standalone blocks can also be attached to an agent as tools.
In the lead scorer, the Agent has a Search tool and a Send Email tool. For a lead with a thin profile it runs Search to gather context; for a strong lead it calls Send Email. A thin, obvious lead might trigger neither. The agent chooses per run.
Each tool carries a `usageControl` setting. **Auto** lets the model decide (the default). **Force** makes the agent call the tool every run, for actions that should never be skipped, like always logging the decision. **None** removes the tool from that agent.
A block and an agent tool can be the same underlying integration. The difference is who decides. A block runs because the path reached it. An agent tool runs because the agent chose it.
## Custom tool [#custom-tool]
A **custom tool** is a tool you define once with an object schema and a JavaScript function body, then reuse across your workspace. It needs no external account. Use one when you have logic that several agents or workflows would otherwise duplicate.
In the lead scorer, a `normalizeCompanyDomain` custom tool cleans a raw website into a canonical domain. The same tool serves the lead scorer, a deduplication workflow, and a reporting agent. Define it in the workspace, then pick it from any Agent block's tool list.
## MCP server [#mcp-server]
**MCP** (Model Context Protocol) is a standard for connecting an external tool provider. Connect an [MCP server](/agents/mcp) and its tools appear in the agent's tool list as a set. Use it to bring in a complete toolset that Agent Serve does not provide natively, rather than wiring each action by hand.
In the lead scorer, your CRM vendor ships an MCP server. After you connect it once, the agent can read accounts and update records through the vendor's own tools. The difference from a custom tool is who maintains it: a custom tool is code you wrote, while an MCP server is a toolbox someone else maintains.
## Workflow-as-tool [#workflow-as-tool]
A **workflow-as-tool** is a whole workflow handed to an agent as one callable tool. You pick the workflow in the [Agent](/workflows/blocks/agent) block's tool list; the agent decides when to call it and supplies the inputs, which arrive at the child's [Start](/workflows/triggers/start) trigger, and the child's result comes back as the tool's output. Use it when a multi-step procedure should be at the agent's disposal, not on the path.
In the lead scorer, the agent has a `Deep Enrich` workflow as a tool — its own five-step procedure. For a thin lead, the agent calls it to fill out the profile before scoring; for a complete lead, it never runs. The agent weighs a whole procedure the same way it weighs a single action.
A workflow can also run as a fixed step: the [Workflow](/workflows/blocks/workflow) block calls a child workflow because the path reached it. Same child workflow, same Start trigger — the difference, as with blocks and agent tools, is who decides. The Enrich step in the diagram is that deterministic case.
## Skill [#skill]
A **skill** is reusable instructions, a written playbook an agent can follow. Each skill has a short name and description that are always visible to the agent, plus a longer body the agent loads only when it decides the skill applies. Use one to capture how something should be done, separate from the tools that do it.
In the lead scorer, a `lead-scoring-rubric` skill spells out the bands and disqualifiers. The agent sees the skill's name and description on every run, and when a lead is ambiguous it loads the full rubric and applies it. The distinction is simple: a tool is an action the agent takes, and a skill is guidance the agent reads. Manage skills in your [workspace](/agents/skills).
| Feature | Who decides it runs | Where it lives | How you author it |
| -------------------- | ------------------- | ------------------ | --------------------------------------- |
| **Block** | The path | The workflow | Drag in and configure |
| **Agent tool** | The agent | On the Agent block | Pick from the integrations |
| **Custom tool** | The agent | The workspace | Write the code once |
| **MCP server** | The agent | An external server | Connect it |
| **Workflow-as-tool** | The agent | Its own workflow | Build it, then pick it in the tool list |
| **Skill** | The agent | The workspace | Write the instructions |
---
# Custom Tools (/agents/custom-tools)
Custom tools let you write your own JavaScript functions and make them available as callable tools in Agent blocks. This is useful when you need functionality that isn't covered by Agent Serve's built-in integrations — for example, calling an internal API, performing a custom calculation, or transforming data in a specific way.
## How Custom Tools Work [#how-custom-tools-work]
A custom tool has two parts:
1. **Schema** — A JSON definition describing the tool's name, description, and parameters (using the OpenAI function-calling format). This tells the AI agent what the tool does and what inputs it expects.
2. **Code** — A JavaScript function body that runs when the agent calls the tool. Parameters defined in the schema are available as variables in your code.
When an Agent block has access to a custom tool, the AI model decides when to call it based on the schema description and the conversation context — just like built-in tools.
## Creating a Custom Tool [#creating-a-custom-tool]
### Open Custom Tools settings [#open-custom-tools-settings]
Navigate to **Settings → Custom Tools** in your workspace and click **Add**.
### Define the schema [#define-the-schema]
In the **Schema** tab, define your tool using JSON in the OpenAI function-calling format:
```json
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature units"
}
},
"required": ["city"]
}
}
}
```
You can use the AI wand button to generate a schema from a natural language description of what the tool should do.
### Write the code [#write-the-code]
Switch to the **Code** tab and write the JavaScript function body. Parameters from your schema are available directly as variables:
```javascript
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${units === 'celsius' ? 'metric' : 'imperial'}&appid={{OPENWEATHER_API_KEY}}`
);
const data = await response.json();
return {
temperature: data.main.temp,
description: data.weather[0].description,
humidity: data.main.humidity
};
```
For a secret used as a complete JavaScript expression, prefer the unquoted form, such as `const apiKey = {{OPENWEATHER_API_KEY}};`. Quoted and embedded forms remain supported, including the placeholder embedded in the URL above, `"Bearer {{KEY}}"`, template literals, and JavaScript regex literals. The value is bound separately when the tool executes rather than pasted into its source, so its exact string contents are preserved.
You can also use the AI wand to generate code from a description. Environment variables are referenced with `{{KEY}}` syntax.
### Save [#save]
Click **Save** to create the tool. It's now available to use in any Agent block across your workspace.
## Using Custom Tools in Workflows [#using-custom-tools-in-workflows]
Once created, custom tools appear alongside built-in tools when configuring an Agent block:
1. Open an Agent block
2. Click **Add Tools**
3. Find your custom tool in the tool list
4. The agent will call the tool when it determines it's relevant to the task
## Code Environment [#code-environment]
### Available Features [#available-features]
* **Async/await** — Your code runs in an async context, so you can use `await` directly
* **fetch()** — Make HTTP requests to external APIs
* **Node.js built-ins** — Access to `crypto`, `Buffer`, and other standard modules
* **Environment variables** — Use `{{KEY}}` syntax to bind secrets at execution time without placing plaintext in source code
### Limitations [#limitations]
* **No npm packages** — External libraries like `axios` or `lodash` are not available. Use built-in APIs instead
* **Parameters by name** — Schema parameters are available directly as variables (e.g., `city`), not via a `params` object
### Returning Results [#returning-results]
Return a value from your code to send it back to the agent:
```javascript
const result = await fetch(`https://api.example.com/data?q=${query}`);
const data = await result.json();
return data;
```
The returned value becomes the tool output that the agent sees and can use in its response.
## Managing Custom Tools [#managing-custom-tools]
From **Settings → Custom Tools** you can:
* **Search** tools by name, function name, or description
* **Edit** any tool's schema or code
* **Delete** tools that are no longer needed
Deleting a custom tool removes it from all Agent blocks that reference it. Make sure no active workflows depend on the tool before deleting.
## Permissions [#permissions]
| Action | Required Permission |
| -------------------- | --------------------------------- |
| View custom tools | **Read**, **Write**, or **Admin** |
| Create or edit tools | **Write** or **Admin** |
| Delete tools | **Admin** |
---
# Agents (/agents)
An **agent** is a [workflow](/workflows) that reasons and acts on its own. It reads an input, decides what to do, and carries it out, calling tools and using your data along the way. You build a custom agent in Agent Serve by composing a workflow whose thinking runs through one or more [Agent blocks](/workflows/blocks/agent).
An **Agent block** is the reasoning step inside that workflow: a model reads the values available to it, decides, and returns a result that later blocks read by reference. A simple agent is a single Agent block; a larger one wires several together with other blocks. The Agent block is where the model thinks; the rest of the workflow is what it acts on and through.
The example throughout is an agent that scores inbound sales leads.
## The Agent block [#the-agent-block]
You set up the reasoning step by giving the Agent block a **model** and a **prompt**. The model is the LLM that powers it; you pick one from the available providers, and the default is `claude-sonnet-4-6`. The prompt is a system message that defines who the agent is and how it should behave, plus a user message that carries the input, usually a reference like ``.
When it runs, the Agent block reasons, calls any tools it needs, and stores its result under its own name. By default that result is free text in `content`, read by a later block as ``, alongside run details like the model used, token counts, tool calls, and cost. Every setting and output field is in the [Agent block reference](/workflows/blocks/agent).
## What you give an agent [#what-you-give-an-agent]
On its own, an Agent block can only reason and write text. You extend it so it can act, follow your rules, use your data, remember, and return results other blocks can rely on. Each feature maps to something you'd want an agent to do.
### Take an action: tools [#take-an-action-tools]
To let an agent do something in the world, give it **tools**. A tool is an action the agent can call, like sending an email, searching the web, updating a CRM record, or running another workflow. You attach tools to the Agent block, and the agent decides which to call for the task in front of it. In the lead scorer, the agent has a search tool to gather context on a thin profile and an email tool to reach out to a strong lead.
Tools come from a few places:
* **[Integrations](/integrations)** are the catalog of external services: Gmail, Slack, Airtable, Linear, and hundreds more.
* **[Custom tools](/agents/custom-tools)** are tools you define once with a schema and a snippet of code, then reuse.
* **[MCP tools](/agents/mcp)** come from an external provider you connect through the Model Context Protocol.
* **[Workflow-as-tool](/workflows)** makes another workflow callable, so the agent runs a whole procedure as one step.
The same integration can run two ways. As a [block](/workflows#blocks) it runs because the path reached it. As an agent tool it runs because the agent chose it. Per-tool usage controls (force a tool, or disable one) are in the [Agent block reference](/workflows/blocks/agent).
### Follow a procedure: skills [#follow-a-procedure-skills]
To give an agent instructions it can follow, write a [skill](/agents/skills). A skill is a reusable playbook with a short name and description the agent always sees, plus a longer body it loads only when the skill applies. In the lead scorer, a `lead-scoring-rubric` skill spells out the bands and disqualifiers, and the agent reads the full rubric only when a lead is ambiguous. A tool is an action the agent takes; a skill is guidance it reads.
### Use your documents: knowledge [#use-your-documents-knowledge]
To let an agent answer from your own content, connect a [knowledge base](/knowledgebase). The agent searches it and grounds its answers in what it finds, instead of relying only on the model's general training. Give the lead scorer a knowledge base of past deals and it can compare a new lead against ones you've closed before.
### Remember across runs: memory [#remember-across-runs-memory]
To let an agent reuse information from one run to the next, give it [memory](/workflows/blocks/agent#memory), which stores and recalls values keyed to a conversation. Without it, each run starts fresh; with it, an agent in a chat carries what was said earlier into later messages.
### Return a usable result: structured output [#return-a-usable-result-structured-output]
To make an agent's result something later blocks can act on, give it a **structured output**: a typed object you define instead of free text. In the lead scorer, the agent returns `{ score, tier, reason }`, and a later [Condition](/workflows/blocks/condition) block reads `` to branch. See [how blocks pass data](/workflows/data-flow) for reading fields.
## Choosing what to use [#choosing-what-to-use]
Start with one Agent block and a prompt, then add only what the task needs: a tool when the agent should act, a skill when it needs written guidance, a knowledge base when it should answer from your documents, memory when it should remember, and a structured output when a later block has to read its result.
---
# Using MCP tools (/agents/mcp)
The Model Context Protocol ([MCP](https://modelcontextprotocol.com/)) is an open standard for connecting AI to external tools and data. Add an MCP server to your workspace and its tools become available to your agents — a way to integrate services Agent Serve doesn't have a built-in integration for.
## Adding an MCP Server as a Tool [#adding-an-mcp-server-as-a-tool]
MCP servers provide collections of tools that your agents can use.
To add one:
1. Navigate to **Settings → Tool connections**
2. Click **Add server** to open the configuration modal
3. Enter a **Server Name** and **Server URL**
4. Add any required **Headers** (e.g. API keys)
5. Click **Add server** to save
You can also configure MCP servers directly from the toolbar in an Agent block for quick setup.
### Server Configuration Options [#server-configuration-options]
| Field | Description |
| ------------- | ---------------------------------------------------- |
| **Name** | Display name for the server |
| **URL** | The MCP server endpoint |
| **Transport** | Currently supports `streamable-http` |
| **Headers** | Key-value pairs for authentication or custom headers |
| **Timeout** | Connection timeout in milliseconds (default: 30,000) |
### Environment Variables in Configuration [#environment-variables-in-configuration]
Server URLs and headers support environment variable substitution using `{{VAR_NAME}}` syntax. This keeps sensitive values like API keys out of the server configuration.
```
URL: https://api.example.com/mcp
Authorization: Bearer {{MCP_API_TOKEN}}
```
When you type `{{` in the URL or header fields, a dropdown appears showing available workspace environment variables.
When a saved secret is successfully substituted this way, exact occurrences of its value are masked in stored MCP tool-call traces. The real URL or header value still reaches the MCP server unchanged. See [Execution log protection](/platform/credentials#execution-log-protection) for the exact scope and limitations.
### Testing and Validation [#testing-and-validation]
Click **Test Connection** before saving to verify the server is reachable and discover available tools. The test response shows the number of tools found and the protocol version.
After saving, each server displays its available tools with parameter names, types, and required flags. If a server's tools change (e.g., after a server update), click **Refresh** to fetch the latest schemas. This automatically updates any agent blocks using those tools.
Tool validation badges appear on servers with issues — for example, if a tool was removed from the server but is still referenced in a workflow. Click the badge to see which workflows are affected.
### Domain Allowlisting [#domain-allowlisting]
Self-hosted deployments can restrict which MCP server domains are allowed by setting the `ALLOWED_MCP_DOMAINS` environment variable (comma-separated list). When set, only servers on approved domains can be added. When unset, all domains are allowed.
This governs which domains may be used. It is separate from where those domains are allowed to resolve: an MCP server on a private address is reached by naming it in `EGRESS_ALLOWED_HOSTS` or `EGRESS_ALLOWED_IP_RANGES`, described in [Security](/platform/self-hosting/security#the-ssrf-boundary). Both checks apply.
The allowlist covers the server URL itself. If the server requires OAuth, any endpoint its metadata names on a *different* origin than the server you configured is treated as content rather than as configuration, so that one has to be publicly routable. Endpoints on the server's own origin keep the server's reachability.
## Using MCP Tools in Agents [#using-mcp-tools-in-agents]
Once MCP servers are configured, their tools become available within your agent blocks:
1. Open an **Agent** block
2. In the **Tools** section, click **Add tool…**
3. Under **MCP Servers**, click a server to see its tools
4. Select individual tools, or choose **Configure operations access** for a dynamic server attachment
5. The agent can now access these tools during execution
If you haven't configured a server yet, click **Add MCP Server** at the top of the dropdown to open the setup modal without leaving the block.
## Standalone MCP Block [#standalone-mcp-block]
Use the MCP block to discover operations or run one operation with explicit inputs:
Choose an **Action**:
* **List operations** discovers authorized operation names, descriptions, and input schemas without executing provider operations. Filter by name or description, set a page size from 1 to 100, and pass `nextCursor` into the next request while `hasMore` is true. An authorized list can be empty.
* **Run operation** executes one exact operation name. Configured operations keep their generated argument fields. For an operation name resolved at runtime, supply a JSON arguments object; Agent Serve validates it against the operation's discovered schema before execution.
**MCP Server** takes one shared-server ID or managed-connection ID. Agent Serve resolves a managed connection's parent server internally and verifies workspace and credential access. Both actions accept the same ID; List operations returns that ID as `serverId`, alongside the discovered `operations` and pagination metadata.
The standalone block's Basic fields select a configured connection and discovered operation. Advanced fields accept literal IDs/names or upstream references. A runtime server reference requires JSON arguments. Listing hides the operation and argument fields.
### Operations access [#operations-access]
The **MCP Server (Advanced)** Agent attachment takes a server/connection ID or upstream reference in a plain input. Its **Tool IDs** field accepts exact MCP tool names, such as `search_docs`, entered directly. These are the names returned by List operations, without an Agent Serve server prefix. Neither field uses a server or operation catalog picker.
Agent attachments have three access modes:
| Mode | Behavior |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Only selected** | Allows only selected exact operation names. An empty selection allows nothing. Newly discovered names stay excluded. |
| **All except selected** | Denies selected exact names. An empty selection allows everything otherwise permitted. Denied names stay saved if they temporarily disappear. |
| **All permitted** | Allows every operation available to the authorized credential. |
New restricted configurations start with an empty explicit selection. Existing saved workflows retain their prior access through normalization, while current organization and credential authorization still apply.
Operation restrictions are saved in workflow state on the Agent attachment. Tool IDs are literal configuration, not upstream references or model arguments. An operation must be available to the resolved, authorized connection and permitted by the saved restriction. The standalone block runs its explicitly specified operation and has no separate access policy.
Discovery filters the tools exposed to the Agent. Execution checks the actual server, connection, and saved restriction again before calling the provider. Missing or forbidden operations, unverifiable schemas, malformed arguments, and incorrect connection scopes fail the call. An Agent attachment with no permitted operations fails clearly.
Policies match exact, case-sensitive MCP tool names on whichever authorized connection resolves at runtime. They do not inspect operation arguments: allowing a generic `execute_sql` operation does not limit which SQL it can execute.
## When to Use MCP Tool vs Agent [#when-to-use-mcp-tool-vs-agent]
| Feature | **Agent with MCP tools** | **MCP Tool block** |
| -------------- | -------------------------------- | -------------------------------------- |
| **Execution** | AI decides which tools to call | Deterministic — runs the tool you pick |
| **Parameters** | AI chooses at runtime | You set them explicitly |
| **Best for** | Dynamic, conversational flows | Structured, repeatable steps |
| **Reasoning** | Handles complex multi-step logic | One tool, one call |
## Permission Requirements [#permission-requirements]
MCP functionality requires specific workspace permissions:
| Action | Required Permission |
| ---------------------------- | --------------------------------- |
| Create or update MCP servers | **Write** or **Admin** |
| Delete MCP servers | **Admin** |
| Use MCP tools in agents | **Write** or **Admin** |
| View available MCP tools | **Read**, **Write**, or **Admin** |
| Execute MCP Tool blocks | **Read**, **Write**, or **Admin** |
MCP servers run with the permissions of the user who configured them — only connect servers you trust, and review a server's tools before handing them to agents.
---
# Playbooks (agent skills) (/agents/skills)
Playbooks are reusable packages of instructions that give your AI agents specialized capabilities. The app calls them **Playbooks**; inside the workflow builder and the open format they are called **skills**, and this page uses both words for the same thing. Based on the open [Agent Skills](https://agentskills.io) format, skills let you capture domain expertise, workflows, and best practices that agents can load on demand.
## How Skills Work [#how-skills-work]
Skills use **progressive disclosure** to keep agent context lean:
1. **Discovery** — Only skill names and descriptions are included in the agent's system prompt (\~50-100 tokens each)
2. **Activation** — When the agent decides a skill is relevant, it calls the `load_skill` tool to load the full instructions into context
3. **Execution** — The agent follows the loaded instructions to complete the task
## Creating Skills [#creating-skills]
Playbooks live next to Integrations: click **Chat** in the left rail, choose **Integrations** in the Chat pane, then switch to the **Playbooks** tab (you can also open **Playbooks** from the [command palette](/workspace/command-palette)). It lists every playbook in the workspace, searchable by name and filterable by **All playbooks**, **Created by me**, **Custom**, and **Built-in**. Click a playbook to open its detail page, where you edit, share, and delete it.
Click **Create playbook** to open the **New playbook** page. It takes three fields:
| Field | Description |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | A kebab-case identifier (e.g. `sql-expert`, `code-reviewer`). Max 64 characters. |
| **Description** | A short explanation of what the skill does and when to use it. This is what the agent reads to decide whether to activate the skill. Max 1024 characters. |
| **Content** | The full skill instructions in markdown. This is loaded when the agent activates the skill. |
The description is critical — it's the only thing the agent sees before deciding to load a skill. Be specific about when and why the skill should be used.
### Importing skills [#importing-skills]
Bring in an existing skill in the open [SKILL.md](https://agentskills.io/specification) format two ways:
* **Import** — the **Import** action on the create page takes a `.md` file with YAML frontmatter, or a `.zip` containing a `SKILL.md`.
* **Paste content** — paste the `SKILL.md` straight into **Content**. The frontmatter carries the `name` and `description`; the markdown body is the content.
Integration pages suggest **curated skills** for their service — open one (HubSpot, for example) and add a suggested skill with one click.
### Writing Good Skill Content [#writing-good-skill-content]
Skill content follows the same conventions as [SKILL.md files](https://agentskills.io/specification):
```markdown
# SQL Expert
## When to use this skill
Use when the user asks you to write, optimize, or debug SQL queries.
## Instructions
1. Always ask which database engine (PostgreSQL, MySQL, SQLite)
2. Use CTEs over subqueries for readability
3. Add index recommendations when relevant
4. Explain query plans for optimization requests
## Common Patterns
...
```
**Recommended structure:**
* **When to use** — Specific triggers and scenarios
* **Instructions** — Step-by-step guidance with numbered lists
* **Examples** — Input/output samples showing expected behavior
* **Common Patterns** — Reusable approaches for frequent tasks
* **Edge Cases** — Gotchas and special considerations
Keep skills focused and under 500 lines. If a skill grows too large, split it into multiple specialized skills.
## Skill Editors [#skill-editors]
Everyone in the workspace sees and uses every skill — including members who join later. Nobody needs to be added to a skill to use it.
Each skill has an explicit **editors** list. Editors can edit the skill, delete it, and manage the editors list. Workspace admins can always do this too — they are editors of every skill automatically and cannot be removed from the list. Whoever creates a skill becomes an editor.
Open a playbook from the Playbooks tab to manage it. The detail page has the editable fields, a **Share** action for adding editors from your workspace members, and the **Skill Editors** list at the bottom.
The editors list controls who can edit a skill — it never affects who can see, use, or run it. A workflow that references a skill always executes it, no matter who runs the workflow. Treat skill content as shared team instructions, not as a secret.
## Using Skills in Chat [#using-skills-in-chat]
Skills work in Chat too. Type `/` in the message box to open the skills menu, then pick a skill — or keep typing to filter by name. The skill appears in your message as a tag, e.g. `/format-markdown`.
Tagging a skill loads its full instructions into the conversation, so Agent Serve follows them for that request — no waiting for Agent Serve to decide the skill is relevant on its own.
## Adding Skills to an Agent [#adding-skills-to-an-agent]
Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to.
Selected skills appear as cards that you can click to edit or remove.
### What Happens at Runtime [#what-happens-at-runtime]
When the workflow runs:
1. The agent's system prompt includes an `` section listing each skill's name and description
2. A `load_skill` tool is automatically added to the agent's available tools
3. When the agent determines a skill is relevant to the current task, it calls `load_skill` with the skill name
4. The full skill content is returned as a tool response, giving the agent detailed instructions
This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed.
## When to use a skill [#when-to-use-a-skill]
Use a skill for reusable procedures or knowledge shared across agents, such as a company style guide or a debugging checklist. Keep task-specific instructions in the Agent block.
## Best Practices [#best-practices]
**Writing Effective Descriptions**
* **Be specific and keyword-rich** — Instead of "Helps with SQL", write "Write optimized SQL queries for PostgreSQL, MySQL, and SQLite, including index recommendations and query plan analysis"
* **Include activation triggers** — Mention specific words or phrases that should prompt the skill (e.g., "Use when the user mentions PDFs, forms, or document extraction")
* **Keep it under 200 words** — Agents scan descriptions quickly; make every word count
**Skill Scope and Organization**
* **One skill per domain** — A focused `sql-expert` skill works better than a broad `database-everything` skill
* **Limit to 5-10 skills per agent** — More skills = more decision overhead; start small and add as needed
* **Split large skills** — If a skill exceeds 500 lines, break it into focused sub-skills
**Content Structure**
* **Use markdown formatting** — Headers, lists, and code blocks help agents parse and follow instructions
* **Provide examples** — Show input/output pairs so agents understand expected behavior
* **Be explicit about edge cases** — Don't assume agents will infer special handling
**Testing and Iteration**
* **Test activation** — Run your workflow and verify the agent loads the skill when expected
* **Check for false positives** — Make sure skills aren't activating when they shouldn't
* **Refine descriptions** — If a skill isn't loading when needed, add more keywords to the description
## Learn More [#learn-more]
* [Agent Skills specification](https://agentskills.io) — The open format for portable agent skills
* [Example skills](https://github.com/anthropics/skills) — Browse community skill examples
* [Best practices](https://agentskills.io/what-are-skills) — Writing effective skills
---
# Authentication (/api-reference/authentication)
The Agent Serve API accepts API keys and, when enabled by your deployment, OAuth access tokens. API keys support automation and the SDKs. OAuth lets the CLI and registered applications act on your behalf with permissions you approve.
Agent Serve supports two types of API keys — **personal keys** and **workspace keys** — each with different billing and access behaviors.
## Key Types [#key-types]
| Feature | **Personal Keys** | **Workspace Keys** |
| --------------- | ------------------------------------------ | --------------------------- |
| **Billing** | Workspace payer for workspace-hosted usage | Workspace payer |
| **Scope** | Across workspaces you have access to | Shared across the workspace |
| **Managed by** | Each user individually | Workspace admins |
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
Personal keys identify the user making a request; they do not select who pays.
Hosted usage is billed to the workspace's organization or personal billing
account and, for organizations, is attributed to the actor's member cap.
Workspace admins can disable personal API key usage for their workspace. If
disabled, only workspace keys can be used.
## Generating API Keys [#generating-api-keys]
To generate a personal key, open **Account settings** → **Agent Serve API keys**. Workspace
administrators can create shared keys from **Workspace settings** → **Agent Serve API keys**.
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.
## Using API Keys [#using-api-keys]
Pass your API key in the `X-API-Key` header with every request:
```bash
curl -X POST https://agentserve.valnoraelric.com/api/v2/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"input": {}}'
```
```typescript
const response = await fetch(
'https://agentserve.valnoraelric.com/api/v2/workflows/{workflowId}/execute',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.AGENT_SERVE_API_KEY!,
},
body: JSON.stringify({ input: {} }),
}
)
```
```python
import requests
response = requests.post(
"https://agentserve.valnoraelric.com/api/v2/workflows/{workflowId}/execute",
headers={
"Content-Type": "application/json",
"X-API-Key": os.environ["AGENT_SERVE_API_KEY"],
},
json={"input": {}},
)
```
## Where Keys Are Used [#where-keys-are-used]
API keys authenticate access to:
* **Workflow execution** — run deployed workflows via the API
* **Logs API** — query workflow execution logs and metrics
* **MCP servers** — authenticate connections to deployed MCP servers
* **SDKs** — the [Python](/api-reference/python) and [TypeScript](/api-reference/typescript) SDKs use API keys for all operations
## OAuth access tokens [#oauth-access-tokens]
Use `agentserve login` to authorize the CLI in your browser, or `agentserve login --read-only` to request read access. The CLI stores the login locally and refreshes access tokens automatically. See [CLI authentication](/cli/authentication) for profiles, sign-in, and sign-out.
Registered OAuth applications send access tokens in the `Authorization` header:
```bash
curl https://agentserve.valnoraelric.com/api/v2/workspaces \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
| Scope | Access |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| `api:read` | Read operations, including searches sent as POST requests |
| `api:write` | Includes `api:read`, plus mutations and execution, including operations that can start external work |
| `offline_access` | Refresh tokens for continued access after the access token expires |
Scopes limit what an application may do; your current workspace membership and role still apply. Each endpoint documents its required scope. Some GET endpoints that perform external discovery require `api:write`, so HTTP method alone does not determine the permission.
Manage grants in **Settings** → **General** → **Authorized apps**. Revoking an application signs out all of its logins. `agentserve logout` revokes the current CLI login and removes it from your machine. The Python and TypeScript SDKs currently use API keys; they do not manage OAuth sign-in or refresh tokens.
## Security [#security]
* Keys use the `sk-as-` prefix and are encrypted at rest
* Keys can be revoked at any time from the dashboard
* Use environment variables to store keys — never hardcode them in source code
* For browser-based applications, use a backend proxy to avoid exposing keys to the client
Never expose your API key in client-side code. Use a server-side proxy to make authenticated requests on behalf of your frontend.
---
# Getting Started (/api-reference/getting-started)
## Base URL [#base-url]
All API requests are made to:
```
https://agentserve.valnoraelric.com
```
## OpenAPI specification [#openapi-specification]
Download the [complete OpenAPI 3.1 specification](/openapi.json) as JSON for client generation, request validation, and API tooling.
## Quick Start [#quick-start]
### Get your API key [#get-your-api-key]
Open **Account settings** → **Agent Serve API keys** to create a personal key, or **Workspace settings** → **Agent Serve API keys** for a workspace key. These examples and the SDKs use API keys; the CLI also supports browser sign-in with `agentserve login`. See [Authentication](/api-reference/authentication) for key types and OAuth permissions.
### Find your workflow ID [#find-your-workflow-id]
Open a workflow in the Agent Serve editor. The workflow ID is in the URL:
```
https://agentserve.valnoraelric.com/workspace/{workspaceId}/w/{workflowId}
```
You can also use the [List Workflows](/api-reference/workflows/listWorkflows) endpoint to get all workflow IDs in a workspace.
### Deploy your workflow [#deploy-your-workflow]
A workflow must be deployed before it can be executed via the API. Click the **Deploy** button in the editor toolbar, or use the dashboard to manage deployments.
### Make your first request [#make-your-first-request]
```bash
curl -X POST https://agentserve.valnoraelric.com/api/v2/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"input": {}}'
```
```typescript
const response = await fetch(
`https://agentserve.valnoraelric.com/api/v2/workflows/${workflowId}/execute`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.AGENT_SERVE_API_KEY!,
},
body: JSON.stringify({ input: {} }),
}
)
const data = await response.json()
console.log(data.data.output)
```
```python
import requests
import os
response = requests.post(
f"https://agentserve.valnoraelric.com/api/v2/workflows/{workflow_id}/execute",
headers={
"Content-Type": "application/json",
"X-API-Key": os.environ["AGENT_SERVE_API_KEY"],
},
json={"input": {}},
)
data = response.json()
print(data["data"]["output"])
```
## Sync vs Async Execution [#sync-vs-async-execution]
By default, workflow executions are **synchronous** — the API blocks until the workflow completes and returns the result directly.
For long-running workflows, use **asynchronous execution** by passing `async: true`:
```bash
curl -X POST https://agentserve.valnoraelric.com/api/v2/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"input": {}, "async": true}'
```
Keyed callers can optionally provide `X-Run-Id: my-run-123` to choose the run ID. Run IDs cannot be reused; a duplicate returns `409`.
This returns immediately with a `runId` and `statusUrl`:
```json
{
"data": {
"runId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74",
"statusUrl": "https://agentserve.valnoraelric.com/api/v2/workflows/{workflowId}/runs/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74"
}
}
```
Poll the run status endpoint until the status is terminal:
```bash
curl https://agentserve.valnoraelric.com/api/v2/workflows/{workflowId}/runs/{runId}?includeOutput=true \
-H "X-API-Key: YOUR_API_KEY"
```
Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `data.output` field is populated for completed executions when `includeOutput=true`.
## Response Format [#response-format]
Successful v2 responses wrap the run resource in `data`:
```json
{
"data": {
"runId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74",
"workflowId": "{workflowId}",
"status": "completed",
"output": { "result": "Hello, world!" },
"error": null,
"durationMs": 842
}
}
```
## Error Handling [#error-handling]
The API uses standard HTTP status codes. v2 errors include a stable code and human-readable message:
```json
{
"error": {
"code": "NOT_FOUND",
"message": "Workflow not found"
}
}
```
| Status | Meaning | What to do |
| ------ | -------------------------- | --------------------------------------------------- |
| `400` | Invalid request parameters | Check the `details` array for specific field errors |
| `401` | Missing or invalid API key | Verify your `X-API-Key` header |
| `403` | Access denied | Check you have permission for this resource |
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
### Unrecognized fields are rejected [#unrecognized-fields-are-rejected]
Every v2 endpoint validates the request against its published schema — path parameters, query string, and body — and answers `400` for any field it does not declare. A misspelled parameter is an error rather than a silent no-op, so `?limt=20` fails instead of quietly returning an unbounded list.
This holds for endpoints that declare no query parameters at all. Do not append tracking tags, cache busters, or other extra parameters to a v2 URL; send only what the endpoint documents.
```json
{
"error": {
"code": "BAD_REQUEST",
"message": "Invalid request",
"details": [
{ "code": "unrecognized_keys", "keys": ["limt"], "path": [], "message": "Unrecognized key: \"limt\"" }
]
}
}
```
Use [Get Billing Status](/api-reference/billing/getBillingStatus) to inspect current credit and storage usage.
## Rate Limits [#rate-limits]
Rate limits depend on your subscription plan and apply separately to synchronous and asynchronous executions.
When rate limited, the API returns a `429` response with a `Retry-After` header indicating how many seconds to wait before retrying.
## Pagination [#pagination]
List endpoints (workflows, logs, audit logs) use **cursor-based pagination**:
```bash
# First page
curl "https://agentserve.valnoraelric.com/api/v2/logs?limit=20" \
-H "X-API-Key: YOUR_API_KEY"
# Next page — use the nextCursor from the previous response
curl "https://agentserve.valnoraelric.com/api/v2/logs?limit=20&cursor=abc123" \
-H "X-API-Key: YOUR_API_KEY"
```
The response includes a `nextCursor` field. When `nextCursor` is absent or `null`, you have reached the last page.
---
# Python (/api-reference/python)
Use the Python SDK to execute workflows from Python applications.
The Python SDK supports Python 3.8+ with async execution support, retry helpers with exponential backoff, and usage tracking.
## Installation [#installation]
Install the SDK using pip:
```bash
pip install agentserve-sdk
```
## Quick Start [#quick-start]
Here's a simple example to get you started:
```python
from agentserve import AgentServeClient
# Initialize the client
client = AgentServeClient(
api_key="your-api-key-here",
base_url="https://agentserve.valnoraelric.com" # optional, defaults to https://agentserve.valnoraelric.com
)
# Execute a workflow
try:
result = client.execute_workflow("workflow-id")
print("Workflow executed successfully:", result)
except Exception as error:
print("Workflow execution failed:", error)
```
## API Reference [#api-reference]
### AgentServeClient [#agentserveclient]
#### Constructor [#constructor]
```python
AgentServeClient(api_key: str, base_url: str = "https://agentserve.valnoraelric.com")
```
**Parameters:**
* `api_key` (str): Your Agent Serve API key
* `base_url` (str, optional): Base URL for the Agent Serve API
#### Methods [#methods]
##### execute\_workflow() [#execute_workflow]
Execute a workflow with optional input data.
```python
result = client.execute_workflow(
"workflow-id",
input={"message": "Hello, world!"},
timeout=30.0 # 30 seconds
)
```
**Parameters:**
* `workflow_id` (str): The ID of the workflow to execute
* `input` (dict, optional): Input data to pass to the workflow
* `timeout` (float, optional): Timeout in seconds (default: 30.0)
* `stream` (bool, optional): Enable streaming responses (default: False)
* `selected_outputs` (list\[str], optional): Block outputs to stream in `blockName.attribute` format (e.g., `["agent1.content"]`)
* `async_execution` (bool, optional): Execute asynchronously (default: False)
* `execution_timeout_seconds` (int, optional): Optional server-side async execution cap from 1 to 604800 seconds. Requires `async_execution=True` and cannot extend the account policy.
**Returns:** `WorkflowExecutionResult | AsyncExecutionResult`
When `async_execution=True`, returns immediately with a `run_id` and `status_url` for polling. Otherwise, waits for completion.
##### get\_workflow\_status() [#get_workflow_status]
Get the status of a workflow (deployment status, etc.).
```python
status = client.get_workflow_status("workflow-id")
print("Is deployed:", status.is_deployed)
```
**Parameters:**
* `workflow_id` (str): The ID of the workflow
**Returns:** `WorkflowStatus`
##### validate\_workflow() [#validate_workflow]
Validate that a workflow is ready for execution.
```python
is_ready = client.validate_workflow("workflow-id")
if is_ready:
# Workflow is deployed and ready
pass
```
**Parameters:**
* `workflow_id` (str): The ID of the workflow
**Returns:** `bool`
##### get\_workflow\_run() [#get_workflow_run]
Get the status and optional outputs of a workflow execution.
```python
status = client.get_workflow_run("workflow-id", "run-id", include_output=True)
print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed'
if status["status"] == "completed":
print("Output:", status["output"])
```
**Parameters:**
* `workflow_id` (str): The workflow ID
* `run_id` (str): The run ID returned from async execution
* `include_output` (bool, optional): Include the final output for completed executions
* `selected_outputs` (list\[str], optional): Block output selectors to include
**Returns:** `Dict[str, Any]`
**Response fields:**
* `runId` (str): The run ID
* `workflowId` (str): The workflow ID
* `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'`
* `startedAt` / `endedAt` (str): Execution timestamps
* `durationMs` (int, optional): Duration in milliseconds
* `output` (any, optional): The workflow output when requested for a completed execution
* `blockOutputs` (dict, optional): Requested block outputs
* `error` (dict, optional): Structured failure details with `code`, `message`, and optional `details`
##### get\_job\_status() [#get_job_status]
Get the status of a job created through the legacy async execution endpoint. New integrations should use `get_workflow_run()` with the run ID instead.
```python
status = client.get_job_status("legacy-job-id")
```
##### execute\_with\_retry() [#execute_with_retry]
Execute a workflow with automatic retry on rate limit errors using exponential backoff.
```python
result = client.execute_with_retry(
"workflow-id",
input={"message": "Hello"},
timeout=30.0,
max_retries=3, # Maximum number of retries
initial_delay=1.0, # Initial delay in seconds
max_delay=30.0, # Maximum delay in seconds
backoff_multiplier=2.0 # Exponential backoff multiplier
)
```
**Parameters:**
* `workflow_id` (str): The ID of the workflow to execute
* `input` (dict, optional): Input data to pass to the workflow
* `timeout` (float, optional): Timeout in seconds
* `stream` (bool, optional): Enable streaming responses
* `selected_outputs` (list, optional): Block outputs to stream
* `async_execution` (bool, optional): Execute asynchronously
* `max_retries` (int, optional): Maximum number of retries (default: 3)
* `initial_delay` (float, optional): Initial delay in seconds (default: 1.0)
* `max_delay` (float, optional): Maximum delay in seconds (default: 30.0)
* `backoff_multiplier` (float, optional): Backoff multiplier (default: 2.0)
**Returns:** `WorkflowExecutionResult | AsyncExecutionResult`
The retry logic uses exponential backoff (1s → 2s → 4s → 8s...) with ±25% jitter to prevent thundering herd. If the API provides a `retry-after` header, it will be used instead.
##### get\_rate\_limit\_info() [#get_rate_limit_info]
Get the current rate limit information from the last API response.
```python
rate_limit_info = client.get_rate_limit_info()
if rate_limit_info:
print("Limit:", rate_limit_info.limit)
print("Remaining:", rate_limit_info.remaining)
print("Reset:", datetime.fromtimestamp(rate_limit_info.reset))
```
**Returns:** `RateLimitInfo | None`
##### get\_usage\_limits() [#get_usage_limits]
Get current usage limits and quota information for your account.
```python
limits = client.get_usage_limits()
print("Sync requests remaining:", limits.rate_limit["sync"]["remaining"])
print("Async requests remaining:", limits.rate_limit["async"]["remaining"])
print("Current period cost:", limits.usage["currentPeriodCost"])
print("Plan:", limits.usage["plan"])
```
**Returns:** `UsageLimits`
**Response structure:**
```python
{
"success": bool,
"rateLimit": {
"sync": {
"isLimited": bool,
"limit": int,
"remaining": int,
"resetAt": str
},
"async": {
"isLimited": bool,
"limit": int,
"remaining": int,
"resetAt": str
},
"authType": str # 'api' or 'manual'
},
"usage": {
"currentPeriodCost": float,
"limit": float,
"plan": str # e.g., 'free', 'pro'
}
}
```
##### set\_api\_key() [#set_api_key]
Update the API key.
```python
client.set_api_key("new-api-key")
```
##### set\_base\_url() [#set_base_url]
Update the base URL.
```python
client.set_base_url("https://my-custom-domain.com")
```
##### close() [#close]
Close the underlying HTTP session.
```python
client.close()
```
## Data Classes [#data-classes]
### WorkflowExecutionResult [#workflowexecutionresult]
```python
@dataclass
class WorkflowExecutionResult:
success: bool
output: Optional[Any] = None
error: Optional[str] = None
logs: Optional[List[Any]] = None
metadata: Optional[Dict[str, Any]] = None
trace_spans: Optional[List[Any]] = None
total_duration: Optional[float] = None
status: Optional[str] = None
```
`success` is `True` only for the `completed` and `paused` statuses. `status` carries the server's terminal status verbatim, so a cancelled run (`success=False`, `error=None`) is distinguishable from a failed one.
### AsyncExecutionResult [#asyncexecutionresult]
```python
@dataclass
class AsyncExecutionResult:
success: bool
run_id: str
status_url: str
message: str = ""
async_execution: bool = True
```
### WorkflowStatus [#workflowstatus]
```python
@dataclass
class WorkflowStatus:
is_deployed: bool
deployed_at: Optional[str] = None
needs_redeployment: bool = False
```
### RateLimitInfo [#ratelimitinfo]
```python
@dataclass
class RateLimitInfo:
limit: int
remaining: int
reset: int
retry_after: Optional[int] = None
```
### UsageLimits [#usagelimits]
```python
@dataclass
class UsageLimits:
success: bool
rate_limit: Dict[str, Any]
usage: Dict[str, Any]
```
### AgentServeError [#agentserveerror]
```python
class AgentServeError(Exception):
def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None):
super().__init__(message)
self.code = code
self.status = status
```
**Common error codes:**
* `UNAUTHORIZED`: Invalid API key
* `TIMEOUT`: Request timed out
* `RATE_LIMIT_EXCEEDED`: Rate limit exceeded
* `USAGE_LIMIT_EXCEEDED`: Usage limit exceeded
* `EXECUTION_ERROR`: Workflow execution failed
## Examples [#examples]
### Basic Workflow Execution [#basic-workflow-execution]
Set up the AgentServeClient with your API key.
Check if the workflow is deployed and ready for execution.
Run the workflow with your input data.
Process the execution result and handle any errors.
```python
import os
from agentserve import AgentServeClient
client = AgentServeClient(api_key=os.getenv("AGENT_SERVE_API_KEY"))
def run_workflow():
try:
# Check if workflow is ready
is_ready = client.validate_workflow("my-workflow-id")
if not is_ready:
raise Exception("Workflow is not deployed or ready")
# Execute the workflow
result = client.execute_workflow(
"my-workflow-id",
input={
"message": "Process this data",
"user_id": "12345"
}
)
if result.success:
print("Output:", result.output)
print("Duration:", result.metadata.get("duration") if result.metadata else None)
else:
print("Workflow failed:", result.error)
except Exception as error:
print("Error:", error)
run_workflow()
```
### Error Handling [#error-handling]
Handle different types of errors that may occur during workflow execution:
```python
from agentserve import AgentServeClient, AgentServeError
import os
client = AgentServeClient(api_key=os.getenv("AGENT_SERVE_API_KEY"))
def execute_with_error_handling():
try:
result = client.execute_workflow("workflow-id")
return result
except AgentServeError as error:
if error.code == "UNAUTHORIZED":
print("Invalid API key")
elif error.code == "TIMEOUT":
print("Workflow execution timed out")
elif error.code == "USAGE_LIMIT_EXCEEDED":
print("Usage limit exceeded")
elif error.code == "INVALID_JSON":
print("Invalid JSON in request body")
else:
print(f"Workflow error: {error}")
raise
except Exception as error:
print(f"Unexpected error: {error}")
raise
```
### Context Manager Usage [#context-manager-usage]
Use the client as a context manager to automatically handle resource cleanup:
```python
from agentserve import AgentServeClient
import os
# Using context manager to automatically close the session
with AgentServeClient(api_key=os.getenv("AGENT_SERVE_API_KEY")) as client:
result = client.execute_workflow("workflow-id")
print("Result:", result)
# Session is automatically closed here
```
### Batch Workflow Execution [#batch-workflow-execution]
Execute multiple workflows efficiently:
```python
from agentserve import AgentServeClient
import os
client = AgentServeClient(api_key=os.getenv("AGENT_SERVE_API_KEY"))
def execute_workflows_batch(workflow_data_pairs):
"""Execute multiple workflows with different input data."""
results = []
for workflow_id, input_data in workflow_data_pairs:
try:
# Validate workflow before execution
if not client.validate_workflow(workflow_id):
print(f"Skipping {workflow_id}: not deployed")
continue
result = client.execute_workflow(workflow_id, input_data)
results.append({
"workflow_id": workflow_id,
"success": result.success,
"output": result.output,
"error": result.error
})
except Exception as error:
results.append({
"workflow_id": workflow_id,
"success": False,
"error": str(error)
})
return results
# Example usage
workflows = [
("workflow-1", {"type": "analysis", "data": "sample1"}),
("workflow-2", {"type": "processing", "data": "sample2"}),
]
results = execute_workflows_batch(workflows)
for result in results:
print(f"Workflow {result['workflow_id']}: {'Success' if result['success'] else 'Failed'}")
```
### Async Workflow Execution [#async-workflow-execution]
Execute workflows asynchronously for long-running tasks:
```python
import os
import time
from agentserve import AgentServeClient
client = AgentServeClient(api_key=os.getenv("AGENT_SERVE_API_KEY"))
def execute_async():
try:
# Start async execution
result = client.execute_workflow(
"workflow-id",
input={"data": "large dataset"},
async_execution=True # Execute asynchronously
)
# Check if result is an async execution
if hasattr(result, 'async_execution') and result.async_execution:
print(f"Run ID: {result.run_id}")
print(f"Status endpoint: {result.status_url}")
# Poll for completion
status = client.get_workflow_run(
"workflow-id", result.run_id, include_output=True
)
while status["status"] in ["queued", "pending", "running"]:
print(f"Current status: {status['status']}")
time.sleep(2) # Wait 2 seconds
status = client.get_workflow_run(
"workflow-id", result.run_id, include_output=True
)
if status["status"] == "completed":
print("Workflow completed!")
print(f"Output: {status['output']}")
print(f"Duration: {status['durationMs']}")
elif status["status"] == "paused":
print("Workflow is paused and waiting for input or resumption.")
elif status["status"] == "cancelled":
print("Workflow was cancelled.")
else:
print(f"Workflow failed: {status['error']}")
except Exception as error:
print(f"Error: {error}")
execute_async()
```
### Rate Limiting and Retry [#rate-limiting-and-retry]
Handle rate limits automatically with exponential backoff:
```python
import os
from agentserve import AgentServeClient, AgentServeError
client = AgentServeClient(api_key=os.getenv("AGENT_SERVE_API_KEY"))
def execute_with_retry_handling():
try:
# Automatically retries on rate limit
result = client.execute_with_retry(
"workflow-id",
input={"message": "Process this"},
max_retries=5,
initial_delay=1.0,
max_delay=60.0,
backoff_multiplier=2.0
)
print(f"Success: {result}")
except AgentServeError as error:
if error.code == "RATE_LIMIT_EXCEEDED":
print("Rate limit exceeded after all retries")
# Check rate limit info
rate_limit_info = client.get_rate_limit_info()
if rate_limit_info:
from datetime import datetime
reset_time = datetime.fromtimestamp(rate_limit_info.reset)
print(f"Rate limit resets at: {reset_time}")
execute_with_retry_handling()
```
### Usage Monitoring [#usage-monitoring]
Monitor your account usage and limits:
```python
import os
from agentserve import AgentServeClient
client = AgentServeClient(api_key=os.getenv("AGENT_SERVE_API_KEY"))
def check_usage():
try:
limits = client.get_usage_limits()
print("=== Rate Limits ===")
print("Sync requests:")
print(f" Limit: {limits.rate_limit['sync']['limit']}")
print(f" Remaining: {limits.rate_limit['sync']['remaining']}")
print(f" Resets at: {limits.rate_limit['sync']['resetAt']}")
print(f" Is limited: {limits.rate_limit['sync']['isLimited']}")
print("\nAsync requests:")
print(f" Limit: {limits.rate_limit['async']['limit']}")
print(f" Remaining: {limits.rate_limit['async']['remaining']}")
print(f" Resets at: {limits.rate_limit['async']['resetAt']}")
print(f" Is limited: {limits.rate_limit['async']['isLimited']}")
print("\n=== Usage ===")
print(f"Current period cost: ${limits.usage['currentPeriodCost']:.2f}")
print(f"Limit: ${limits.usage['limit']:.2f}")
print(f"Plan: {limits.usage['plan']}")
percent_used = (limits.usage['currentPeriodCost'] / limits.usage['limit']) * 100
print(f"Usage: {percent_used:.1f}%")
if percent_used > 80:
print("⚠️ Warning: You are approaching your usage limit!")
except Exception as error:
print(f"Error checking usage: {error}")
check_usage()
```
### Streaming Workflow Execution [#streaming-workflow-execution]
Execute workflows with real-time streaming responses:
```python
from agentserve import AgentServeClient
import os
client = AgentServeClient(api_key=os.getenv("AGENT_SERVE_API_KEY"))
def execute_with_streaming():
"""Execute workflow with streaming enabled."""
try:
# Enable streaming for specific block outputs
result = client.execute_workflow(
"workflow-id",
input={"message": "Count to five"},
stream=True,
selected_outputs=["agent1.content"] # Use blockName.attribute format
)
print("Workflow result:", result)
except Exception as error:
print("Error:", error)
execute_with_streaming()
```
The streaming response follows the Server-Sent Events (SSE) format:
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"}
data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
**Flask Streaming Example:**
```python
from flask import Flask, Response, stream_with_context
import requests
import json
import os
app = Flask(__name__)
@app.route('/stream-workflow')
def stream_workflow():
"""Stream workflow execution to the client."""
def generate():
response = requests.post(
'https://agentserve.valnoraelric.com/api/v2/workflows/WORKFLOW_ID/execute',
headers={
'Content-Type': 'application/json',
'X-API-Key': os.getenv('AGENT_SERVE_API_KEY')
},
json={
'input': {'message': 'Generate a story'},
'stream': True,
'selectedOutputs': ['agent1.content']
},
stream=True
)
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data: '):
data = decoded_line[6:] # Remove 'data: ' prefix
if data == '[DONE]':
break
try:
parsed = json.loads(data)
if 'chunk' in parsed:
yield f"data: {json.dumps(parsed)}\n\n"
elif parsed.get('event') == 'done':
yield f"data: {json.dumps(parsed)}\n\n"
print("Execution complete:", parsed.get('metadata'))
except json.JSONDecodeError:
pass
return Response(
stream_with_context(generate()),
mimetype='text/event-stream'
)
if __name__ == '__main__':
app.run(debug=True)
```
### Environment Configuration [#environment-configuration]
Configure the client using environment variables:
```python
import os
from agentserve import AgentServeClient
# Development configuration
client = AgentServeClient(
api_key=os.getenv("AGENT_SERVE_API_KEY"),
base_url=os.getenv("AGENT_SERVE_BASE_URL", "https://agentserve.valnoraelric.com")
)
```
```python
import os
from agentserve import AgentServeClient
# Production configuration with error handling
api_key = os.getenv("AGENT_SERVE_API_KEY")
if not api_key:
raise ValueError("AGENT_SERVE_API_KEY environment variable is required")
client = AgentServeClient(
api_key=api_key,
base_url=os.getenv("AGENT_SERVE_BASE_URL", "https://agentserve.valnoraelric.com")
)
```
## Getting Your API Key [#getting-your-api-key]
Create a key in **Account settings → Agent Serve API keys**, or use a workspace key if your administrator requires one. See [Authentication](/api-reference/authentication) for key types and permissions. Deploy the workflow before calling it through the SDK. Keep the key in a server-side environment variable.
## Requirements [#requirements]
* Python 3.8+
* requests >= 2.25.0
---
# TypeScript (/api-reference/typescript)
Use the TypeScript SDK to execute workflows from server-side JavaScript or TypeScript. For browser applications, call the SDK through your authenticated backend.
The TypeScript SDK provides full type safety, async execution support, retry helpers with exponential backoff, and usage tracking.
## Installation [#installation]
Install the SDK using your preferred package manager:
```bash
npm install @agentserve/sdk
```
```bash
yarn add @agentserve/sdk
```
```bash
bun add @agentserve/sdk
```
## Quick Start [#quick-start]
Here's a simple example to get you started:
```typescript
import { AgentServeClient } from '@agentserve/sdk';
// Initialize the client
const client = new AgentServeClient({
apiKey: 'your-api-key-here',
baseUrl: 'https://agentserve.valnoraelric.com' // optional, defaults to https://agentserve.valnoraelric.com
});
// Execute a workflow
try {
const result = await client.executeWorkflow('workflow-id');
console.log('Workflow executed successfully:', result);
} catch (error) {
console.error('Workflow execution failed:', error);
}
```
## API Reference [#api-reference]
### AgentServeClient [#agentserveclient]
#### Constructor [#constructor]
```typescript
new AgentServeClient(config: AgentServeConfig)
```
**Configuration:**
* `config.apiKey` (string): Your Agent Serve API key
* `config.baseUrl` (string, optional): Base URL for the Agent Serve API (defaults to `https://agentserve.valnoraelric.com`)
#### Methods [#methods]
##### executeWorkflow() [#executeworkflow]
Execute a workflow with optional input data.
```typescript
const result = await client.executeWorkflow('workflow-id', { message: 'Hello, world!' }, {
timeout: 30000 // 30 seconds
});
```
**Parameters:**
* `workflowId` (string): The ID of the workflow to execute
* `input` (any, optional): Input data to pass to the workflow
* `options` (ExecutionOptions, optional):
* `timeout` (number): Timeout in milliseconds (default: 30000)
* `stream` (boolean): Enable streaming responses (default: false)
* `selectedOutputs` (string\[]): Block outputs to stream in `blockName.attribute` format (e.g., `["agent1.content"]`)
* `async` (boolean): Execute asynchronously (default: false)
* `executionTimeoutSeconds` (number): Optional server-side async execution cap from 1 to 604800 seconds. Requires `async: true` and cannot extend the account policy.
**Returns:** `Promise`
When `async: true`, returns immediately with a `runId` and `statusUrl` for polling. Otherwise, waits for completion.
##### getWorkflowStatus() [#getworkflowstatus]
Get the status of a workflow (deployment status, etc.).
```typescript
const status = await client.getWorkflowStatus('workflow-id');
console.log('Is deployed:', status.isDeployed);
```
**Parameters:**
* `workflowId` (string): The ID of the workflow
**Returns:** `Promise`
##### validateWorkflow() [#validateworkflow]
Validate that a workflow is ready for execution.
```typescript
const isReady = await client.validateWorkflow('workflow-id');
if (isReady) {
// Workflow is deployed and ready
}
```
**Parameters:**
* `workflowId` (string): The ID of the workflow
**Returns:** `Promise`
##### getWorkflowRun() [#getworkflowrun]
Get the status and optional outputs of a workflow run.
```typescript
const status = await client.getWorkflowRun('workflow-id', 'run-id', {
includeOutput: true
});
console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed'
if (status.status === 'completed') {
console.log('Output:', status.output);
}
```
**Parameters:**
* `workflowId` (string): The workflow ID
* `runId` (string): The run ID returned from async execution
* `options.includeOutput` (boolean, optional): Include the final output for completed executions
* `options.selectedOutputs` (string\[], optional): Block output selectors to include
**Returns:** `Promise`
**Response fields:**
* `runId` (string): The run ID
* `workflowId` (string): The workflow ID
* `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'`
* `startedAt` / `endedAt` (string): Execution timestamps
* `durationMs` (number, nullable): Duration in milliseconds
* `output` (any, nullable): The workflow output when requested for a completed execution
* `blockOutputs` (object, nullable): Requested block outputs
* `error` (object, nullable): Structured failure details with `code`, `message`, and optional `details`
##### getJobStatus() [#getjobstatus]
Get the status of a job created through the legacy async execution endpoint. New integrations should use `getWorkflowRun()` with the run ID instead.
```typescript
const status = await client.getJobStatus('legacy-job-id');
```
##### executeWithRetry() [#executewithretry]
Execute a workflow with automatic retry on rate limit errors using exponential backoff.
```typescript
const result = await client.executeWithRetry('workflow-id', { message: 'Hello' }, {
timeout: 30000
}, {
maxRetries: 3, // Maximum number of retries
initialDelay: 1000, // Initial delay in ms (1 second)
maxDelay: 30000, // Maximum delay in ms (30 seconds)
backoffMultiplier: 2 // Exponential backoff multiplier
});
```
**Parameters:**
* `workflowId` (string): The ID of the workflow to execute
* `input` (any, optional): Input data to pass to the workflow
* `options` (ExecutionOptions, optional): Same as `executeWorkflow()`
* `retryOptions` (RetryOptions, optional):
* `maxRetries` (number): Maximum number of retries (default: 3)
* `initialDelay` (number): Initial delay in ms (default: 1000)
* `maxDelay` (number): Maximum delay in ms (default: 30000)
* `backoffMultiplier` (number): Backoff multiplier (default: 2)
**Returns:** `Promise`
The retry logic uses exponential backoff (1s → 2s → 4s → 8s...) with ±25% jitter to prevent thundering herd. If the API provides a `retry-after` header, it will be used instead.
##### getRateLimitInfo() [#getratelimitinfo]
Get the current rate limit information from the last API response.
```typescript
const rateLimitInfo = client.getRateLimitInfo();
if (rateLimitInfo) {
console.log('Limit:', rateLimitInfo.limit);
console.log('Remaining:', rateLimitInfo.remaining);
console.log('Reset:', new Date(rateLimitInfo.reset * 1000));
}
```
**Returns:** `RateLimitInfo | null`
##### getUsageLimits() [#getusagelimits]
Get current usage limits and quota information for your account.
```typescript
const limits = await client.getUsageLimits();
console.log('Sync requests remaining:', limits.rateLimit.sync.remaining);
console.log('Async requests remaining:', limits.rateLimit.async.remaining);
console.log('Current period cost:', limits.usage.currentPeriodCost);
console.log('Plan:', limits.usage.plan);
```
**Returns:** `Promise`
**Response structure:**
```typescript
{
success: boolean
rateLimit: {
sync: {
isLimited: boolean
limit: number
remaining: number
resetAt: string
}
async: {
isLimited: boolean
limit: number
remaining: number
resetAt: string
}
authType: string // 'api' or 'manual'
}
usage: {
currentPeriodCost: number
limit: number
plan: string // e.g., 'free', 'pro'
}
}
```
##### setApiKey() [#setapikey]
Update the API key.
```typescript
client.setApiKey('new-api-key');
```
##### setBaseUrl() [#setbaseurl]
Update the base URL.
```typescript
client.setBaseUrl('https://my-custom-domain.com');
```
## Types [#types]
### WorkflowExecutionResult [#workflowexecutionresult]
```typescript
interface WorkflowExecutionResult {
success: boolean;
output?: any;
error?: string;
logs?: any[];
metadata?: {
duration?: number;
runId?: string;
[key: string]: any;
};
traceSpans?: any[];
totalDuration?: number;
}
```
### AsyncExecutionResult [#asyncexecutionresult]
```typescript
interface AsyncExecutionResult {
success: boolean;
runId: string;
statusUrl: string;
message: string;
async: true;
}
```
### WorkflowStatus [#workflowstatus]
```typescript
interface WorkflowStatus {
isDeployed: boolean;
deployedAt?: string;
needsRedeployment: boolean;
}
```
### RateLimitInfo [#ratelimitinfo]
```typescript
interface RateLimitInfo {
limit: number;
remaining: number;
reset: number;
retryAfter?: number;
}
```
### UsageLimits [#usagelimits]
```typescript
interface UsageLimits {
success: boolean;
rateLimit: {
sync: {
isLimited: boolean;
limit: number;
remaining: number;
resetAt: string;
};
async: {
isLimited: boolean;
limit: number;
remaining: number;
resetAt: string;
};
authType: string;
};
usage: {
currentPeriodCost: number;
limit: number;
plan: string;
};
}
```
### AgentServeError [#agentserveerror]
```typescript
class AgentServeError extends Error {
code?: string;
status?: number;
}
```
**Common error codes:**
* `UNAUTHORIZED`: Invalid API key
* `TIMEOUT`: Request timed out
* `RATE_LIMIT_EXCEEDED`: Rate limit exceeded
* `USAGE_LIMIT_EXCEEDED`: Usage limit exceeded
* `EXECUTION_ERROR`: Workflow execution failed
## Examples [#examples]
### Basic Workflow Execution [#basic-workflow-execution]
Set up the AgentServeClient with your API key.
Check if the workflow is deployed and ready for execution.
Run the workflow with your input data.
Process the execution result and handle any errors.
```typescript
import { AgentServeClient } from '@agentserve/sdk';
const client = new AgentServeClient({
apiKey: process.env.AGENT_SERVE_API_KEY!
});
async function runWorkflow() {
try {
// Check if workflow is ready
const isReady = await client.validateWorkflow('my-workflow-id');
if (!isReady) {
throw new Error('Workflow is not deployed or ready');
}
// Execute the workflow
const result = await client.executeWorkflow('my-workflow-id', {
message: 'Process this data',
userId: '12345'
});
if (result.success) {
console.log('Output:', result.output);
console.log('Duration:', result.metadata?.duration);
} else {
console.error('Workflow failed:', result.error);
}
} catch (error) {
console.error('Error:', error);
}
}
runWorkflow();
```
### Error Handling [#error-handling]
Handle different types of errors that may occur during workflow execution:
```typescript
import { AgentServeClient, AgentServeError } from '@agentserve/sdk';
const client = new AgentServeClient({
apiKey: process.env.AGENT_SERVE_API_KEY!
});
async function executeWithErrorHandling() {
try {
const result = await client.executeWorkflow('workflow-id');
return result;
} catch (error) {
if (error instanceof AgentServeError) {
switch (error.code) {
case 'UNAUTHORIZED':
console.error('Invalid API key');
break;
case 'TIMEOUT':
console.error('Workflow execution timed out');
break;
case 'USAGE_LIMIT_EXCEEDED':
console.error('Usage limit exceeded');
break;
case 'INVALID_JSON':
console.error('Invalid JSON in request body');
break;
default:
console.error('Workflow error:', error.message);
}
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
```
### Environment Configuration [#environment-configuration]
Configure the client using environment variables:
```typescript
import { AgentServeClient } from '@agentserve/sdk';
// Development configuration
const apiKey = process.env.AGENT_SERVE_API_KEY;
if (!apiKey) {
throw new Error('AGENT_SERVE_API_KEY environment variable is required');
}
const client = new AgentServeClient({
apiKey,
baseUrl: process.env.AGENT_SERVE_BASE_URL // optional
});
```
```typescript
import { AgentServeClient } from '@agentserve/sdk';
// Production configuration with validation
const apiKey = process.env.AGENT_SERVE_API_KEY;
if (!apiKey) {
throw new Error('AGENT_SERVE_API_KEY environment variable is required');
}
const client = new AgentServeClient({
apiKey,
baseUrl: process.env.AGENT_SERVE_BASE_URL || 'https://agentserve.valnoraelric.com'
});
```
### Node.js Express Integration [#nodejs-express-integration]
Integrate with an Express.js server:
```typescript
import express from 'express';
import { AgentServeClient } from '@agentserve/sdk';
const app = express();
const client = new AgentServeClient({
apiKey: process.env.AGENT_SERVE_API_KEY!
});
app.use(express.json());
app.post('/execute-workflow', async (req, res) => {
try {
const { workflowId, input } = req.body;
const result = await client.executeWorkflow(workflowId, input, {
timeout: 60000
});
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Workflow execution error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
```
### Next.js API Route [#nextjs-api-route]
Use with Next.js API routes:
```typescript
// pages/api/workflow.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { AgentServeClient } from '@agentserve/sdk';
const client = new AgentServeClient({
apiKey: process.env.AGENT_SERVE_API_KEY!
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { workflowId, input } = req.body;
const result = await client.executeWorkflow(workflowId, input, {
timeout: 30000
});
res.status(200).json(result);
} catch (error) {
console.error('Error executing workflow:', error);
res.status(500).json({
error: 'Failed to execute workflow'
});
}
}
```
### Browser Usage [#browser-usage]
Keep the Agent Serve API key on your server. A browser application should call an authenticated backend endpoint that checks which workflow the user can run, then calls the SDK. The [Next.js API Route](#nextjs-api-route) example shows where the server-side SDK call belongs; add your application authentication and authorization before executing a workflow.
### File Upload [#file-upload]
File objects are automatically detected and converted to base64 format. Include them in your input under the field name matching your workflow's API trigger input format.
The SDK converts File objects to this format:
```typescript
{
type: 'file',
data: 'data:mime/type;base64,base64data',
name: 'filename',
mime: 'mime/type'
}
```
Alternatively, you can manually provide files using the URL format:
```typescript
{
type: 'url',
data: 'https://example.com/file.pdf',
name: 'file.pdf',
mime: 'application/pdf'
}
```
Run file uploads on your backend after authenticating the caller and validating the upload. For example, in Node.js:
```typescript
import { AgentServeClient } from '@agentserve/sdk';
import { readFile } from 'node:fs/promises';
const client = new AgentServeClient({ apiKey: process.env.AGENT_SERVE_API_KEY! });
const fileBuffer = await readFile('./document.pdf');
const file = new File([fileBuffer], 'document.pdf', { type: 'application/pdf' });
const result = await client.executeWorkflow('workflow-id', {
documents: [file],
query: 'Summarize this document'
});
```
### Async Workflow Execution [#async-workflow-execution]
Execute workflows asynchronously for long-running tasks:
```typescript
import { AgentServeClient, AsyncExecutionResult } from '@agentserve/sdk';
const client = new AgentServeClient({
apiKey: process.env.AGENT_SERVE_API_KEY!
});
async function executeAsync() {
try {
// Start async execution
const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, {
async: true // Execute asynchronously
});
// Check if result is an async execution
if ('async' in result && result.async) {
console.log('Run ID:', result.runId);
console.log('Status endpoint:', result.statusUrl);
// Poll for completion
let status = await client.getWorkflowRun('workflow-id', result.runId, {
includeOutput: true
});
while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') {
console.log('Current status:', status.status);
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
status = await client.getWorkflowRun('workflow-id', result.runId, {
includeOutput: true
});
}
if (status.status === 'completed') {
console.log('Workflow completed!');
console.log('Output:', status.output);
console.log('Duration:', status.durationMs);
} else if (status.status === 'paused') {
console.log('Workflow is paused and waiting for input or resumption.');
} else if (status.status === 'cancelled') {
console.log('Workflow was cancelled.');
} else {
console.error('Workflow failed:', status.error);
}
}
} catch (error) {
console.error('Error:', error);
}
}
executeAsync();
```
### Rate Limiting and Retry [#rate-limiting-and-retry]
Handle rate limits automatically with exponential backoff:
```typescript
import { AgentServeClient, AgentServeError } from '@agentserve/sdk';
const client = new AgentServeClient({
apiKey: process.env.AGENT_SERVE_API_KEY!
});
async function executeWithRetryHandling() {
try {
// Automatically retries on rate limit
const result = await client.executeWithRetry('workflow-id', { message: 'Process this' }, {}, {
maxRetries: 5,
initialDelay: 1000,
maxDelay: 60000,
backoffMultiplier: 2
});
console.log('Success:', result);
} catch (error) {
if (error instanceof AgentServeError && error.code === 'RATE_LIMIT_EXCEEDED') {
console.error('Rate limit exceeded after all retries');
// Check rate limit info
const rateLimitInfo = client.getRateLimitInfo();
if (rateLimitInfo) {
console.log('Rate limit resets at:', new Date(rateLimitInfo.reset * 1000));
}
}
}
}
```
### Usage Monitoring [#usage-monitoring]
Monitor your account usage and limits:
```typescript
import { AgentServeClient } from '@agentserve/sdk';
const client = new AgentServeClient({
apiKey: process.env.AGENT_SERVE_API_KEY!
});
async function checkUsage() {
try {
const limits = await client.getUsageLimits();
console.log('=== Rate Limits ===');
console.log('Sync requests:');
console.log(' Limit:', limits.rateLimit.sync.limit);
console.log(' Remaining:', limits.rateLimit.sync.remaining);
console.log(' Resets at:', limits.rateLimit.sync.resetAt);
console.log(' Is limited:', limits.rateLimit.sync.isLimited);
console.log('\nAsync requests:');
console.log(' Limit:', limits.rateLimit.async.limit);
console.log(' Remaining:', limits.rateLimit.async.remaining);
console.log(' Resets at:', limits.rateLimit.async.resetAt);
console.log(' Is limited:', limits.rateLimit.async.isLimited);
console.log('\n=== Usage ===');
console.log('Current period cost: $' + limits.usage.currentPeriodCost.toFixed(2));
console.log('Limit: $' + limits.usage.limit.toFixed(2));
console.log('Plan:', limits.usage.plan);
const percentUsed = (limits.usage.currentPeriodCost / limits.usage.limit) * 100;
console.log('Usage: ' + percentUsed.toFixed(1) + '%');
if (percentUsed > 80) {
console.warn('⚠️ Warning: You are approaching your usage limit!');
}
} catch (error) {
console.error('Error checking usage:', error);
}
}
checkUsage();
```
### Streaming Workflow Execution [#streaming-workflow-execution]
Execute workflows with real-time streaming responses:
```typescript
import { AgentServeClient } from '@agentserve/sdk';
const client = new AgentServeClient({
apiKey: process.env.AGENT_SERVE_API_KEY!
});
async function executeWithStreaming() {
try {
// Enable streaming for specific block outputs
const result = await client.executeWorkflow('workflow-id', { message: 'Count to five' }, {
stream: true,
selectedOutputs: ['agent1.content'] // Use blockName.attribute format
});
console.log('Workflow result:', result);
} catch (error) {
console.error('Error:', error);
}
}
```
The streaming response follows the Server-Sent Events (SSE) format:
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"}
data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
For browser streaming, have your authenticated backend forward the Agent Serve SSE response. Keep the Agent Serve API key on the backend, check the upstream response status, and parse complete SSE events across network chunks. See [streaming responses](/workflows/deployment/api#streaming) for the event format.
## Getting Your API Key [#getting-your-api-key]
Create a key in **Account settings → Agent Serve API keys**, or use a workspace key if your administrator requires one. See [Authentication](/api-reference/authentication) for key types and permissions. Deploy the workflow before calling it through the SDK. Keep the key in a server-side environment variable.
## Requirements [#requirements]
* Node.js 16+
* TypeScript 5.0+ (for TypeScript projects)
---
# Workflow imports and workspace sync (/api-reference/workflow-sync)
An automated client exports or inspects the source, previews destination choices, applies the reviewed request, and polls its operation. The [CLI workflow guide](/cli/workflow-sync) follows this same v2 protocol; the server owns authorization, remapping, atomic writes, and durable completion for both surfaces.
## CLI and HTTP interfaces [#cli-and-http-interfaces]
Paths below are relative to `/api/v2`. `{workspaceId}` is the explicitly selected current workspace.
| CLI command | HTTP request |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `workflows export --include-references` | `GET /workflows/{workflowId}/export?includeReferences=true` |
| `workflows import-preview` / `workflows import` | `POST /workflows/import/preview` / `POST /workflows/import` |
| `workspaces fork-availability` / `lineage` | `GET /workspaces/{workspaceId}/fork/availability` / `.../fork/lineage` |
| `workspaces children` / `fork-resources` | `GET /workspaces/{workspaceId}/fork/children` / `.../fork/resources?kind=tables` |
| `workspaces fork-preview` / `fork` | `POST /workspaces/{workspaceId}/fork/preview` / `.../fork` |
| `workspaces push-preview` / `push --yes` | `POST /workspaces/{workspaceId}/fork/push/preview` / `.../fork/push` |
| `workspaces pull-preview` / `pull --yes` | `POST /workspaces/{workspaceId}/fork/pull/preview` / `.../fork/pull` |
| `workspaces mappings get` / `mappings update` | `GET` / `PUT /workspaces/{workspaceId}/fork/mappings` |
| `selectors list` / `selectors get` | `POST /selectors/list` / `POST /selectors/get` |
| `workspaces operations get ` / `operations wait ` | `GET /workspaces/{workspaceId}/operations/{operationId}`; wait polls this endpoint |
| `workspaces operations list` | `GET /workspaces/{workspaceId}/operations` |
The generated [sync preview reference](/api-reference/workspace-sync/previewWorkspacePull) documents every field; its sidebar includes rollback, unlink, and exclusion endpoints. Children, resource discovery, mappings, and operations are paginated; follow `nextCursor` without changing the query's scope or filters.
## Permissions [#permissions]
Send a personal API key in `X-API-Key`, or an OAuth access token in `Authorization: Bearer …`. Existing workspace policies, credential access, permission groups, Enterprise/self-hosting gates, and workspace creation limits still apply.
| Operation | Required access |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| Export | Workflow read access |
| Import preview and apply | Destination write access; binding credentials also requires an acting user with credential access |
| Fork discovery, preview, and creation | Source workspace admin |
| Sync preview/apply and mapping read/update | Admin on both workspaces on a direct fork edge |
| Rollback / unlink | Target admin / acting-side admin, respectively |
| Selector discovery | Read access in the discovery workspace and access to the selected credential |
| Operation get/list | Read access in the receipt workspace |
Workspace API keys can import where existing authoring policy allows, but cannot bind credentials, administer forks, or execute selectors. Do not substitute a key owner for an acting user. OAuth import/fork/sync previews require `api:write`, even though preview does not commit changes. Export, fork discovery, mapping reads, selectors, and operation reads use `api:read`.
## Portable imports [#portable-imports]
Default exports remain sanitized. `includeReferences=true` adds a versioned manifest of registered resource IDs and source occurrences, including nested tools. It does not include secret values. Imported provenance and source IDs are labels; they never grant access to an alleged source workspace.
Set `BASE_URL` to your deployment, and use an authorized key for each workspace. Raw HTTP returns `{ "data": ... }`; the CLI unwraps single-resource results. Extract the export payload before saving it:
```sh
curl -sS --fail-with-body \
-H "X-API-Key: $SOURCE_API_KEY" \
"$BASE_URL/api/v2/workflows/$WORKFLOW_ID/export?includeReferences=true" \
| jq '.data' > workflow.json
```
Save destination mappings as `import-mappings.json`, replacing the example IDs with manifest and destination resource IDs:
```json
[
{ "kind": "credential", "sourceId": "source-connection", "targetId": "destination-connection" },
{ "kind": "sandbox", "sourceId": "source-sandbox", "targetId": "destination-sandbox" }
]
```
`mappings` applies to every occurrence of a resource's `kind` and `sourceId`. For older exports, `bindings` can address individual registered occurrences. Its entries are flat objects, without `sourceId` or an `occurrence` wrapper:
```json
[
{
"kind": "credential",
"blockId": "source-agent",
"subBlockKey": "tools",
"valuePath": [0, "params", "oauthCredential"],
"encoding": "scalar",
"targetId": "destination-connection"
}
]
```
A top-level field uses `valuePath: []`. Other registered encodings are `array`, `csv`, `files`, and `environment`; multi-value occurrences can include `positions`. Use the actual registered occurrence, rather than inventing paths. Conflicting instructions for one occurrence are rejected. `targetId: null` requests clearing; it cannot satisfy a required binding. Destination type, provider, and parent-child compatibility are validated.
Build one request file and preview it:
```sh
jq -n --arg workspaceId "$DESTINATION_WORKSPACE" \
--slurpfile workflow workflow.json --slurpfile mappings import-mappings.json \
'{workspaceId: $workspaceId, workflow: $workflow[0], mappings: $mappings[0]}' \
> import-request.json
curl -sS --fail-with-body -H "X-API-Key: $DESTINATION_API_KEY" \
-H 'Content-Type: application/json' --data @import-request.json \
"$BASE_URL/api/v2/workflows/import/preview" > import-preview.json
```
Inspect `data.unresolvedBindings`, `data.configuration`, `data.unresolvedConfiguration`, and `data.discovery`. Dependent choices use a different shape from bindings:
```json
[
{ "blockId": "source-agent", "subBlockKey": "tools[0].folder", "value": "destination-label" }
]
```
Add these as `dependentValues` to `import-request.json` and preview again. For a field with `multiSelect: true`, `value` is a comma-separated string of selected IDs. For credential-backed selectors, discover options in the import's destination workspace using the returned `selectorKey` and `context`:
```sh
curl -sS --fail-with-body -H "X-API-Key: $DESTINATION_API_KEY" \
-H 'Content-Type: application/json' \
--data "$(jq -n --arg workspaceId "$DESTINATION_WORKSPACE" \
'{workspaceId: $workspaceId, selectorKey: "gmail.labels", context: {oauthCredential: "destination-connection"}, limit: 50}')" \
"$BASE_URL/api/v2/selectors/list"
```
Selector lists return `{ "data": [...], "nextCursor": null, "truncated": false }`; check both pagination and truncation. Detail uses `/selectors/get` with the same scope/context and an `id`. MCP tools use `mcp.tools` with `mcpServerId`. A missing OAuth connection can require human authorization; changing a resource ID cannot create that connection.
When `data.ready` is true, save a stable request ID and apply the exact reviewed choices:
```sh
jq --arg requestId "$IMPORT_REQUEST_ID" \
--arg fingerprint "$(jq -er '.data.previewFingerprint' import-preview.json)" \
'. + {requestId: $requestId, previewFingerprint: $fingerprint}' \
import-request.json > import-apply.json
curl -sS --fail-with-body -H "X-API-Key: $DESTINATION_API_KEY" \
-H 'Content-Type: application/json' --data @import-apply.json \
"$BASE_URL/api/v2/workflows/import" > import-result.json
```
Mapped import creates a draft, its graph, variables, required inline custom tools, and receipt atomically. Remapping precedes graph ID regeneration; the result includes `idMap`. Plain imports without mapping options retain their earlier behavior and return no operation receipt. Supplying mapping options, even empty arrays, requires `requestId` and `previewFingerprint`.
## Fork and sync [#fork-and-sync]
Fork preview and apply share `{ "name": "Review environment", "copy": { "tables": ["source-table"] } }`. Apply adds `requestId` and the preview's fingerprint. Eligible deployed source workflows become child drafts; resource copies must be selected explicitly.
Push sends deployed workflows from the current workspace to `otherWorkspaceId`. Pull sends them from `otherWorkspaceId` to the current workspace. Either endpoint works from either side of the direct parent/child edge.
For example, save this as `sync-request.json` for a pull into the workspace in the URL:
```json
{
"otherWorkspaceId": "source-workspace",
"mappings": [
{ "resourceType": "oauth_credential", "sourceId": "source-connection", "targetId": "destination-connection" }
],
"dependentValues": [
{ "sourceWorkflowId": "source-workflow", "sourceBlockId": "source-agent", "subBlockKey": "tools[0].folder", "value": "destination-label" }
],
"copyResources": { "tables": ["source-table"] }
}
```
Sync mappings use edge `resourceType` names, such as `oauth_credential`, `service_account_credential`, `knowledge_base`, `custom_tool`, or `sandbox`; imports use `kind`, such as `credential`, `knowledge-base`, or `custom-tool`. Sync workflow identity is system-managed. Pick a knowledge document through its parent KB's dependent selector instead of writing a `knowledge_document` edge mapping.
Mapping inspection requires `otherWorkspaceId` and `direction` in its query. Read rows also contain a storage `id`; write requests accept only `resourceType`, `sourceId`, and `targetId`. Project a page with `jq '[.data[] | {resourceType, sourceId, targetId}]'` before reusing its mappings, and follow `nextCursor` to collect further pages.
Preview never saves inline mappings. Apply persists them with the sync transaction. Dependent overrides use source workflow/block/field identities, including original nested tool indices. Omission reuses saved choices; providing `dependentValues` replaces those choices for affected workflows, and `[]` clears them. Target draft values alone are not saved sync configuration.
```sh
curl -sS --fail-with-body -H "X-API-Key: $AGENT_SERVE_API_KEY" \
-H 'Content-Type: application/json' --data @sync-request.json \
"$BASE_URL/api/v2/workspaces/$CURRENT_WORKSPACE/fork/pull/preview" > sync-preview.json
jq --arg requestId "$SYNC_REQUEST_ID" \
--arg fingerprint "$(jq -er '.data.previewFingerprint' sync-preview.json)" \
'. + {requestId: $requestId, previewFingerprint: $fingerprint, confirm: true}' \
sync-request.json > sync-apply.json
curl -sS --fail-with-body -H "X-API-Key: $AGENT_SERVE_API_KEY" \
-H 'Content-Type: application/json' --data @sync-apply.json \
"$BASE_URL/api/v2/workspaces/$CURRENT_WORKSPACE/fork/pull" > sync-result.json
```
Review `unresolvedBindings`, `configuration`, planned workflow actions, and retiring trigger URLs before apply. Use each configuration field's `discoveryWorkspaceId` for `/selectors/list` or `/selectors/get`: source when its parent will be copied, destination when mapped. Pass its returned context and re-preview any changed choices. A ready preview is not a deployment-readiness report.
`triggerSlots` lists stable `sourceWorkflowId` and `sourceBlockId` identities, `ownPath`, `adoptablePaths`, and `defaultAdoptPath`. A slot with `ownPath` preserves that URL and accepts no override. For an arriving trigger, `triggerMappings` can select an offered retiring path or `null` to request a new URL. Include the same choices on preview and apply:
```json
[
{ "sourceWorkflowId": "source-workflow", "sourceBlockId": "source-trigger", "adoptPath": "retiring-trigger-path" }
]
```
Unknown source identities, duplicate choices, and paths outside that slot's candidates are rejected. Adoption candidates stay within the same target workflow and provider; do not construct them from a different workflow's URL.
Copy selections use `copy` for fork creation and `copyResources` for sync. Fork `copy.files` contains workspace file IDs; sync `copyResources.files` contains storage keys. Credentials and secret values are not copied. `dropReferences` only acknowledges references deleted in the source; it cannot discard live source resources.
Sync replaces eligible target workflows and schedules deployment of the admitted snapshots. Exclusions remain in effect. A deleted source can archive its mapped target; an undeployed source does not. Rollback restores the latest target sync's prior deployed versions, with no promise to restore arbitrary drafts or undo all resource copies.
## Receipts, polling, and retries [#receipts-polling-and-retries]
Apply returns a single-resource envelope. This is an example completed sync report:
```json
{
"data": {
"operationId": "operation-id",
"requestId": "release-2026-09-09",
"workspaceId": "current-workspace",
"kind": "workspace_pull",
"applied": true,
"status": "completed",
"resourceIds": ["destination-workflow"],
"issues": [],
"deployments": [
{
"operationId": "deployment-id",
"workflowId": "destination-workflow",
"version": 2,
"status": "active",
"ready": true,
"pendingComponents": []
}
]
}
}
```
Always poll under the returned `workspaceId`. Import receipts belong to the destination. Fork-creation receipts belong to the source on which `/fork` ran. Push/pull receipts belong to the current workspace in the request URL, including a push that changes the other workspace.
```sh
OPERATION_ID=$(jq -er '.data.operationId' sync-result.json)
OPERATION_WORKSPACE=$(jq -er '.data.workspaceId' sync-result.json)
curl -sS --fail-with-body -H "X-API-Key: $AGENT_SERVE_API_KEY" \
"$BASE_URL/api/v2/workspaces/$OPERATION_WORKSPACE/operations/$OPERATION_ID"
```
Poll with a delay while `status` is `processing`. Terminal outcomes are `completed`, `completed_with_warnings`, `requires_configuration`, and `failed`. Inspect `issues`, `copyProgress`, `deployments[].ready`, `pendingComponents`, and `triggerUrlChanges`. `applied: true` remains true after a follow-up failure: the transaction committed. Completed imports and forks remain drafts; a completed sync requires checking its admitted deployment results before treating the destination as ready.
Request IDs deduplicate within the receipt workspace. Retain the complete apply request: identical authorized retries return the original operation before checking preview freshness. A changed payload under the same ID, a stale preview, or blocked apply returns HTTP `409` with `{ "error": { "code": "CONFLICT", "message": "...", "details": {} } }`. Other invalid inputs can return `400`; follow-up failures are reported on a committed operation.
After an uncertain response, retry the identical request with the original ID, or query `GET /workspaces/{workspaceId}/operations?requestId=...`. Lists return `{ "data": [...], "nextCursor": ... }`; use operation get for refreshed completion status. If a stale preview is refused before commit, obtain a new preview and use a new request ID for the revised request. Never replace a lost-response request with a fresh ID merely to retry.
## What the test harness covers [#what-the-test-harness-covers]
Run `bun run test:workflow-sync` from the Agent Serve repository with Bun, dependencies, and Docker available. It creates disposable PostgreSQL 17, exercises actual authorization, API-key authentication, v2 HTTP adapters, CLI subprocesses, graph/receipt transactions, locks, and deployment outbox workers, then removes the database. Tests cover concurrent retries, stale previews, atomic refusal, immutable deployment snapshots, exclusions, pagination, and copy-worker recovery.
External provider options and failures use controlled fixtures; the separate realtime process uses an authenticated loopback fixture. This validates the platform protocol rather than proving every provider account is connected. Verify provider authorization and destination deployment readiness in the environment you intend to use. Migration safety is checked separately from this fresh-schema harness.
---
# Apps (/apps)
An **app** is a purpose-built tool that runs inside your workspace — a deal desk, a contract review screen, a field visit log. Apps work on your own tables, models, workflows, and Relays, and always act as the person using them.
There are two kinds:
* **Apps your developers build** in this workspace with the CLI and SDK. See [Build on Agent Serve](/developers).
* **[Plugins](/plugins)** — apps that also add Chat commands, menu actions, background triggers, connectors, and skills across the workspace. Agent Serve publishes a set of them in **Discover**.
## The Apps hub [#the-apps-hub]
Open **Apps** from the left rail.
The page has these parts:
* **Installed** — every app installed in this workspace. A card shows **Live**, or **Turned off** when an admin turned it off. If nothing is installed yet, you see **No apps installed yet**.
* **From Agent Serve** — a few plugins from the catalog, with **See all** to open [Discover](/plugins/discover).
* **Ready to install** (admins only) — app versions that are published and can be installed here.
* **Built in this workspace** (builders only) — apps your team is building, with their versions.
The header has **Discover** for everyone, and **Build an app** and **Upload bundle** for people who can build apps.
## Open an app [#open-an-app]
Click an installed app to open it. Apps with several pages show them as tabs across the top. You are already signed in, and the app can only reach what you can reach yourself.
An admin can also put an app on the left rail for people with chosen roles, so it opens in one click. If an app was turned off, it shows **This app is turned off in this workspace.**
When an app wants to make a change in another service through your connected accounts — update a record, send a message — it asks you to confirm first.
## Install an app [#install-an-app]
Only workspace admins can install apps. To install:
1. Find the app — under **Ready to install** on the Apps page, or in [Discover](/plugins/discover) for plugins.
2. Click **Install**. The install review shows what the app can access, what it creates, and any settings. See [Install a plugin](/plugins/install) for every part of the review.
3. Click **Install**. The app appears under **Installed** for everyone in the workspace.
Installing creates the tables, rules, and Relays the app declares, if the workspace does not have them yet.
## Who can do what [#who-can-do-what]
| Role | Can |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Any member** | Open installed apps and use what they add |
| **Builder** (write access, and app building allowed) | Build and deploy apps, open **Manage**, see versions and tools |
| **Admin** (a builder who is also a workspace admin) | Install and uninstall, change settings, budgets, and secrets, and approve background runs |
Apps are turned on per workspace or organization. If you do not see **Apps** on the rail, it is not on for your workspace, or your [permission group](/platform/enterprise/access-control) hides it.
---
# Manage an app (/apps/manage)
Every installed app has a **Manage** page. Open it with **Manage** on the app's card on the Apps page. You need write access and app building allowed to open it; changing settings needs a workspace admin.
The header has **Open app**, **Open in IDE** (for apps built in this workspace), and **Uninstall**.
## Settings [#settings]
| Setting | What it does |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Turned on** | Turn the app off to stop everyone using it, without uninstalling. |
| **Version** | The version running here. |
| **On the rail for** | Put the app on the left rail for people with the **Read**, **Write**, or **Admin** role. |
| **Monthly budget (USD)** | The most the app may spend on paid work — models, media, integrations — each calendar month (UTC). Leave it as it is to use the amount from the app's manifest. |
When an app reaches its budget, its paid calls stop until next month with **This app has used its monthly budget of $X**. **Spend this month** shows how much it has used.
Each app is also limited in how many paid calls it can make per minute — for example 30 AI model calls and 6 Chat answers — so one busy app cannot crowd out the rest of the workspace. Over the limit, calls fail with **This app is making too many … calls; try again shortly**.
## Access [#access]
**Access** lists everything the app can reach — the tables it reads and writes, the Relays it can hand tasks to, the models it uses, and whether it can open Chat. An app can never reach more than the person using it can.
Plugins also show **Resources**, **Plugin settings**, **Background runs**, **Connectors**, **Skills**, and **Runs**. See [Manage plugins](/plugins/manage).
## Tools, secrets, and versions [#tools-secrets-and-versions]
* **Tools** — the app's code tools, each marked **Reads only** or **Makes changes**, with **Test run**.
* **Secrets** — keys the app needs, such as an API key for another service. Only admins can set them.
* **Versions** — every published version, with **Running here**. Use **Run this version** to switch the workspace to another version, **Publish** to make a draft installable, and **Retire** to withdraw one.
* **Activity** — recent changes to the install.
When a newer version is published, Discover shows **Update available**. Switch to it with **Run this version** under **Versions**.
## Uninstall [#uninstall]
Click **Uninstall** and confirm. Members lose the app right away. Tables the app created, and their data, stay in the workspace.
---
# Files & documents (/chat/files)
Describe a document, presentation, image, or visualization and Agent Serve creates it — streaming the content live into the resource panel as it writes. Attach any file to your message and Agent Serve reads it, processes it, and saves it to your workspace.
## Uploading Files to the Workspace [#uploading-files-to-the-workspace]
Attach any file directly to your message in Chat — drag it into the input, paste it, or click the attachment icon. Agent Serve reads the file as context and saves it to your workspace.
Use this to:
* Hand Agent Serve a document and ask it to process, summarize, or extract data from it
* Upload a CSV and have it create a table from it
* Drop in a PDF and ask Agent Serve to turn it into a knowledge base document
* Attach a design mockup and ask Agent Serve to describe it or generate code from it
Uploaded files appear in the [Library](/files) and are accessible to all workflows in the workspace. Agent Serve can also fetch a file directly from a URL and save it for you: "Download the JSON at \[URL] and save it to the workspace."
## Creating Documents [#creating-documents]
Agent Serve can write any text-based file — markdown, plain text, code files, CSV, JSON, or any other format:
* "Write a technical spec for the new auth system as a markdown file"
* "Create a CSV of our test accounts with columns for name, email, and plan tier"
* "Write a Python script that calls our workflow API and processes the response"
* "Draft a postmortem for the outage last Tuesday and save it as a markdown file"
* "Write a personalized outbound email for Acme Corp based on their recent funding announcement"
* "Draft a weekly ops digest summarizing workflow run counts, errors, and top failures for the past 7 days"
Files are saved to your workspace and accessible from the [Library](/files).
## Editing Existing Files [#editing-existing-files]
Open a file using `@filename` or the **+** menu, then describe the change:
* "Update the pricing section to reflect the new tiers"
* "Refactor this Python script to use async/await"
* "Add a section on error handling to this spec"
* "Rewrite the introduction of this report to be more concise"
## Presentations [#presentations]
Agent Serve can generate `.pptx` files:
* "Create a pitch deck for Q3 review — 8 slides covering growth, retention, and roadmap"
* "Turn this research report into a 10-slide presentation"
* "Build a deck that walks through our API onboarding flow"
* "Build a battle card deck for our top 3 competitors — one slide each covering positioning, pricing, and how we win"
* "Create an account plan for Acme Corp — their priorities, our solution fit, and proposed next steps"
The file is saved to your workspace and can be downloaded.
## Images [#images]
Agent Serve can generate images using AI, and can use an existing image as a reference to guide the output:
**Generating images:**
* "Generate a banner image for the new feature announcement — dark background, clean typography"
* "Create a diagram showing the data flow through our webhook pipeline"
* "Make a social card for the blog post with the title and author name"
**Using a reference image:**
* Attach an existing image to your message, then describe what you want: "Generate a new version of this banner with a blue color scheme instead of green"
* "Create a variation of this diagram with the boxes rearranged horizontally \[attach image]"
Generated images are saved as workspace files.
## Charts and Visualizations [#charts-and-visualizations]
Agent Serve can generate charts and data visualizations from data you describe or reference:
* "Plot the workflow run counts from the metrics table as a bar chart grouped by week"
* "Create a line chart of token usage over the past 30 days from this data \[paste data]"
* "Generate a pie chart showing the distribution of lead sources from the leads table"
Visualizations are saved as files and rendered in the resource panel.
## Calculations & Data Processing [#calculations--data-processing]
For one-off calculations and data transformations, describe what you need and Agent Serve runs it directly in the chat:
* "Parse this JSON and extract all records where status is 'failed'"
* "Calculate the p95 latency from these timing values: \[paste values]"
* "Convert these Unix timestamps to ISO 8601"
* "Deduplicate this list of emails, case-insensitive"
Results come back directly in the chat. Ask Agent Serve to save the output as a file if you need it.
## File Viewer Modes [#file-viewer-modes]
When a file opens in the resource panel, you can switch between three views:
| Mode | What it shows |
| ----------- | -------------------------------- |
| **Editor** | Raw editable text |
| **Preview** | Rendered output (markdown, HTML) |
| **Split** | Editor and preview side by side |
---
# Chat (/chat)
Use Chat to build workflows, research a topic, generate files, query tables, schedule jobs, and act through connected integrations. Reference the workspace resources you want Agent Serve to use.
## What You Can Do [#what-you-can-do]
| Area | What Agent Serve can do |
| --------------------------------------------- | -------------------------------------------------------------------------- |
| **[Workflows](/chat/workflows)** | Build, edit, run, debug, deploy, and organize workflows |
| **[Research](/chat/research)** | Search the web, read pages, crawl sites, produce research reports |
| **[Files & Documents](/chat/files)** | Upload, create, edit, and generate documents, presentations, and images |
| **[Tables](/chat/tables)** | Create, query, update, and export workspace tables |
| **[Automation & Configuration](/chat/tasks)** | Schedule jobs, take immediate actions, connect integrations, manage tools |
| **[Knowledge Bases](/chat/knowledge)** | Create knowledge bases, add documents, and query content in plain language |
## How It Works [#how-it-works]
Agent Serve can find workspace resources by name and inspect the details it needs. Reference a resource explicitly when you want to direct its attention:
* "Run the invoice workflow"
* "Add a row to the leads table"
* "Deploy the summarizer as a chat"
No configuration, no context-setting. Just describe what you want:
* "Build a lead enrichment workflow that scores inbound signups and writes the results to the leads table"
* "Research our top 5 competitors and save a battle card for each one"
* "Schedule a daily job that checks for new high-fit prospects and posts them to #outbound in Slack"
* "Create a workflow that takes a contract PDF, extracts the key terms, and emails a summary to legal"
For complex tasks, Agent Serve delegates to specialized subagents automatically. You'll see them appear as collapsible sections in the chat while they work — building, researching, writing files, executing actions.
## Adding Context [#adding-context]
Bring any workspace object into the conversation via the **+** menu, `@`-mentions, or drag-and-drop from the sidebar. Agent Serve also opens resources automatically when it creates or modifies them.
| What to add | How it appears |
| ------------------ | -------------------------------------------------- |
| **Workflow** | Interactive workflow builder in the resource panel |
| **Table** | Full table editor in the resource panel |
| **File** | File viewer with editor, split, and preview modes |
| **Knowledge Base** | Knowledge base management UI |
| **Folder** | Folder contents |
| **Chat** | A previous Chat conversation |
## Layout [#layout]
Chat has two panes. On the left: the chat thread, where your messages and Agent Serve's responses appear. On the right: the resource panel, where workflows, tables, files, and knowledge bases open as tabs. The panel is resizable; tabs are draggable and closeable.
## Models and Effort [#models-and-effort]
The two controls on the right of the composer set how Chat thinks:
* **Model** — **Brain Auto** picks a model for each message. Choose a specific Brain model to pin it, or open **More models** for the rest.
* **Effort** — **Auto** lets Chat decide how much reasoning each message needs. Choose **Fixed** to hold it at the level you set on the slider between **Faster** and **Smarter**.
Each reply shows the model and effort it used under the message.
---
# Knowledge bases (/chat/knowledge)
Create a knowledge base, add documents to it, and query it in plain language — all through conversation. Knowledge bases you create in Chat are immediately available to Agent blocks in any workflow.
## Creating Knowledge Bases [#creating-knowledge-bases]
Describe the knowledge base and Agent Serve creates it:
* "Create a knowledge base called 'Product Docs'"
* "Set up a knowledge base for our support team — call it 'Support KB'"
* "Create a competitive intelligence knowledge base"
* "Create a knowledge base from our sales playbook and attach it to the outbound agent workflow"
* "Set up a customer success knowledge base — I'll add our onboarding guides and past case studies to it"
## Adding Documents [#adding-documents]
Add documents by attaching files to your message, pasting text, or pointing Agent Serve at a URL:
* "Add this PDF to the Product Docs knowledge base \[attach file]"
* "Add the following text to the Support KB as a new document: \[paste content]"
* "Fetch the page at \[URL] and add it to the competitive intelligence knowledge base"
* "Add these three uploaded case studies to the customer success knowledge base"
Agent Serve processes and indexes each document automatically. Once indexed, the content is searchable by any Agent block that has the knowledge base attached.
{/* TODO: Screenshot of Agent Serve confirming a document was added and indexed — showing the document name and its indexed status in the knowledge base. */}
## Querying Knowledge Bases [#querying-knowledge-bases]
Ask Agent Serve a question and it searches the specified knowledge base to answer:
* "What does the Product Docs knowledge base say about our refund policy?"
* "Search the Support KB for anything related to SSO setup errors"
* "What are the key differences between our Pro and Enterprise plans, based on the product docs?"
* "Find everything in the competitive intelligence knowledge base about \[competitor]'s pricing"
## Connectors [#connectors]
For knowledge bases that should stay current automatically, connectors sync content from external services on a schedule — no manual uploads needed. New content is added, changed content is re-processed, and deleted content is removed on every run.
Connectors are configured through the knowledge base settings, not through Chat. Once connected, all synced content is immediately searchable by Agent Serve and by any Agent block with the knowledge base attached.
Use a [connector](/knowledgebase/connectors) to sync sources such as Notion, Google Drive, Slack, GitHub, or Confluence.
Examples of what you can sync:
* **Notion** — sync a workspace, a database, or a specific page tree
* **Google Drive / Dropbox / OneDrive** — sync documents from cloud storage
* **GitHub** — sync a repository's markdown and code files
* **Slack** — sync channel history
* **Confluence / Jira** — sync your internal wiki or issue tracker
* **HubSpot / Salesforce** — sync CRM records into a searchable knowledge base
See [Connectors](/knowledgebase/connectors) for setup steps, sync frequency options, and managing connector status.
## Managing Knowledge Bases [#managing-knowledge-bases]
List, inspect, and clean up knowledge bases in plain language:
* "What knowledge bases are in this workspace?"
* "How many documents are in the Support KB?"
* "Remove the outdated pricing doc from the Product Docs knowledge base"
* "Delete the old-competitive-intel knowledge base"
## Using Knowledge Bases in Workflows [#using-knowledge-bases-in-workflows]
Knowledge bases created in Chat are immediately available to Agent blocks in any workflow. Attach a knowledge base to an Agent block and it will use semantic search to retrieve relevant content at runtime.
See [Knowledge Base](/knowledgebase) for full details on document processing settings, search configuration, and connector syncing.
---
# Agent Serve Mailer (/chat/mailer)
Agent Serve Mailer gives your workspace a dedicated email address. Forward or send emails to it and Agent Serve will process them as tasks — reading the subject, body, and any attachments, then replying to the thread with the result.
## Getting Started [#getting-started]
1. Navigate to **Settings** → **Inbox**
2. Toggle the inbox on
3. Optionally choose a custom address prefix (e.g., `acme` → `acme@`)
4. Copy your inbox address and start sending emails
If you skip the custom prefix, one is generated automatically.
Changing your address creates a new inbox. The old address stops working immediately.
## What You Can Send [#what-you-can-send]
Write your email like you would to a colleague. The subject and body become the task prompt.
**Attachments are fully supported.** Images, PDFs, and documents (up to 10 MB each) are read by Agent Serve and displayed inline in the conversation; images show as previews.
| Good email | Why it works |
| -------------------------------------------------- | ----------------------------------------- |
| "Summarize the attached PDF and list action items" | Clear task with an attachment |
| "What's in this image?" with a photo attached | Agent Serve reads and describes the image |
| "Draft a reply to this forwarded thread" | Uses the email body as context |
## Allowed Senders [#allowed-senders]
Only authorized senders can create tasks. Emails from anyone else are automatically rejected.
* **Workspace members** are allowed by default — no setup needed
* **External senders** can be added manually with an optional label for easy identification
External senders are email addresses that can create inbox tasks. They are not the same as external workspace members, who have workspace access in Agent Serve without joining your organization.
Manage your allowed senders list in **Settings** → **Inbox** → **Allowed Senders**.
## Tracking Tasks [#tracking-tasks]
Every email becomes a task you can track in **Settings** → **Inbox**:
* **Search** by subject, sender, or body content
* **Filter** by status to find what you need
* **Click** any completed or failed task to jump to the full conversation
### Task Statuses [#task-statuses]
| Status | Meaning |
| -------------- | ---------------------------------------------------------------------------- |
| **Received** | Email accepted, queued for processing |
| **Processing** | Agent Serve is actively working on it |
| **Completed** | Done — the result was sent as an email reply |
| **Failed** | Something went wrong during execution |
| **Rejected** | Email blocked (sender not allowed, automated sender, or rate limit exceeded) |
## Conversations [#conversations]
Each email task creates a conversation in your workspace. You can continue the conversation in Chat, and any follow-up emails in the same thread are linked to the same conversation.
---
# Research (/chat/research)
Ask Agent Serve to research anything and it figures out the best approach — searching the web, reading specific pages, crawling sites, looking up technical docs. Just describe what you want to know.
## Asking Questions [#asking-questions]
Ask anything — about a company, a competitor, a market, a technical question, or a specific URL:
* "What did Salesforce, HubSpot, and Gong each ship in the past 30 days? Summarize the key product updates."
* "What's Acme Corp's tech stack, recent hires, and open engineering roles?"
* "Find everything published about \[competitor] in the past 90 days — press, product changes, job postings."
* "What are the current rate limits on the Anthropic API?"
* "Read \[URL] and tell me what changed in this release"
* "What does Stripe's API say about handling webhooks with idempotency keys?"
* "Who are the main players in AI-powered revenue operations, and how do they differentiate?"
Agent Serve returns an answer directly in the chat. For anything that needs a longer written output, ask it to save the result as a file.
## Research Reports [#research-reports]
When you need a structured, saved document rather than a chat answer, ask Agent Serve to write it up. Agent Serve searches, reads, and cross-references multiple sources until it has enough to produce a full report. The output is saved as a file in your workspace and opened in the resource panel.
{/* TODO: Screenshot of a completed research report open in the resource panel as a file — showing a structured markdown document with sections, findings, and citations. */}
* "Research the top 10 AI SDR tools — pricing, features, positioning, and what customers say. Save as a competitive analysis."
* "Do a full market landscape for AI in healthcare diagnostics — major players, funding, use cases, and regulatory environment."
* "Research how our top 5 competitors handle multi-tenant auth — pricing, architecture, and any known vulnerabilities. Write it up as a report."
* "Find every public case study on AI agents in financial compliance from the past 2 years. Summarize the key outcomes and save as a markdown file."
* "Build a battle card for \[competitor] — their positioning, pricing, strengths, weaknesses, and how we win against them."
---
# Tables (/chat/tables)
Create a table from a description or a CSV, query it in plain language, add or update rows, and export the results — all through conversation. Tables open in the resource panel as soon as they're created or referenced.
## Creating Tables [#creating-tables]
Describe the schema and Agent Serve creates the table:
* "Create a leads table with columns for name, email, company, status, and created date"
* "Create a table that matches the structure of this CSV \[attach file]"
* "Set up an errors table with: id (text), message (text), workflow (text), timestamp (date), resolved (boolean)"
* "Create a prospect table for outbound — company, domain, employee count, industry, ICP score, and last contacted date"
* "Set up an enrichment results table to store output from the lead enrichment workflow: email, company, title, LinkedIn URL, fit score"
## Querying Data [#querying-data]
Ask questions about table contents in plain language:
* "How many rows in the leads table have status 'qualified'?"
* "Show me all records from the past 7 days where score is above 0.8"
* "What are the top 5 most common error messages in the failures table?"
* "Are there any duplicate emails in the contacts table?"
* "How many prospects have an ICP score above 0.75 and haven't been contacted in the past 30 days?"
* "What's the conversion rate from 'contacted' to 'meeting booked' in the pipeline table this month?"
Agent Serve translates the question into a structured query and returns the results.
## Adding and Updating Rows [#adding-and-updating-rows]
Add individual rows, bulk-update based on a condition, or delete records — all in plain language:
* "Add a row to the leads table: Acme Corp, [jane@acme.com](mailto:jane@acme.com), status pending"
* "Mark all rows in the queue table as processed where created\_at is before today"
* "Update the price column for all rows where tier is 'pro' to 49"
* "Delete all rows in the test\_events table"
## Exporting [#exporting]
Export a full table or a filtered subset as a CSV. The file is saved to your workspace and can be downloaded or referenced in other workflows:
* "Export the leads table to a CSV"
* "Export all rows where status is 'closed' and save as a file"
## Using Tables in Workflows [#using-tables-in-workflows]
Tables created in Chat are immediately available in workflows via the [Table tool](/integrations/table). Reference a table by name — no additional configuration needed.
---
# Automation & configuration (/chat/tasks)
Agent Serve can act on your behalf right now — send a message, create an issue, call an API — or on a schedule, running a prompt automatically every hour, day, or week. It can also connect integrations, set environment variables, add MCP servers, and create custom tools.
## Scheduled Jobs [#scheduled-jobs]
A scheduled job is a saved Chat prompt that runs on a cron schedule. On each run, Agent Serve reads the current workspace state and executes the job's prompt as if you had just sent it.
### Creating a Job [#creating-a-job]
Describe the recurring task and how often it should run:
* "Every morning at 8am, check the leads table for new entries and post a summary to #sales in Slack"
* "Every Monday at 9am, pull last week's workflow run counts and write a report to the workspace"
* "Run the data sync workflow every 6 hours"
* "On the first of every month, export the billing table to CSV and email it to [finance@example.com](mailto:finance@example.com)"
* "Every weekday at 7am, check for new funding announcements from companies in our ICP and post the top 5 to #market-intel in Slack"
* "Every Sunday night, run the lead enrichment workflow on all prospects added in the past week and update their scores in the table"
* "Daily at 6am, pull the previous day's workflow errors, summarize the top issues, and post to #eng-alerts"
Agent Serve sets the cron expression and stores the job prompt. The first run happens at the next scheduled time.
### Viewing Job Logs [#viewing-job-logs]
* "Show me the last 5 runs of the weekly report job"
* "Did the sync job run successfully this morning?"
* "What did the Monday digest job do last week?"
Logs show run time, status (completed, failed), and a summary of what the agent did.
### Managing Jobs [#managing-jobs]
* "Pause the morning summary job"
* "Change the sync job to run every 3 hours instead of 6"
* "Delete the onboarding digest job"
* "What scheduled jobs are currently active?"
## Taking Direct Action [#taking-direct-action]
For requests that should happen right now — without building a workflow — just ask. Agent Serve acts immediately using the credentials connected to your workspace.
{/* TODO: Screenshot of Chat showing the "Taking action" subagent label active during a direct action — e.g., posting to Slack or sending an email. Shows the subagent inline in the chat thread. */}
| Request | What happens |
| -------------------------------------------------------------------------- | ------------------------------------ |
| "Send a Slack message to #eng that the deploy finished" | Posts to Slack immediately |
| "Email the Q3 report to [jane@example.com](mailto:jane@example.com)" | Sends via connected Gmail or Outlook |
| "Create a GitHub issue: auth tokens not rotating on logout" | Opens an issue in the specified repo |
| "Add a contact to HubSpot: Acme Corp, [ceo@acme.com](mailto:ceo@acme.com)" | Creates the contact via HubSpot API |
| "Call the webhook at \[URL] with this JSON payload" | Makes the HTTP request |
If an integration isn't connected, Agent Serve walks you through connecting it.
## Connecting Integrations [#connecting-integrations]
Agent Serve can connect new OAuth integrations and API credentials on demand:
* "Connect my Google account"
* "Add the Slack workspace for our team"
* "Set up GitHub with my personal access token"
{/* TODO: Screenshot of Agent Serve walking through connecting an integration — e.g., the Integration subagent active with an OAuth prompt or confirmation that a credential was connected. */}
Once connected, Agent Serve can use the account for authorized actions. Configure a workflow to use the intended available credential.
Select the intended account when configuring each integration. Which credentials are available depends on their ownership, sharing settings, and workspace policy. See [credentials](/platform/credentials).
See [Credentials](/platform/credentials) for managing connected accounts.
## Environment Variables [#environment-variables]
Save API keys, connection strings, and configuration under **Secrets**, then reference them with `{{ENV_VAR}}`. Choose personal or workspace scope according to who should use the value. See [secrets](/workflows/variables#environment-variables).
* "Set the DATABASE\_URL environment variable to 'postgres\://...'"
* "Add an OPENAI\_API\_KEY environment variable"
* "Add a WEBHOOK\_SECRET variable for the inbound webhook workflow"
* "Update the SCORING\_API\_URL variable to point to the new endpoint"
* "What environment variables are currently set?"
{/* TODO: Screenshot of Agent Serve confirming an environment variable was set — e.g., a response message showing the variable name was saved. */}
## MCP Servers [#mcp-servers]
MCP (Model Context Protocol) servers expose tools from external services that Agent blocks can call inside workflows. Connecting an MCP server makes all of its tools available in the workflow editor's tool picker — no custom integration code required.
Agent Serve can add and manage MCP servers connected to your workspace:
* "Add the Stripe MCP server using my API key"
* "Remove the old analytics MCP server"
* "What MCP servers are connected to this workspace?"
* "Update the endpoint for the internal tools MCP server to \[URL]"
Once added, MCP tools appear in the workflow editor's tool picker and can be called from any Agent block.
{/* TODO: Screenshot of Agent Serve confirming an MCP server was added or updated — showing the server name and its status. */}
## Custom Tools [#custom-tools]
[Custom tools](/agents/custom-tools) combine a JSON parameter schema with a JavaScript function body. Use one for an internal API call, calculation, or reusable transformation, then add it to an Agent block's tools.
Agent Serve can build custom tools from a description:
* "Create a custom tool that calls our internal scoring API at \[URL] with a POST request and returns the score field"
* "Build a tool for our Zendesk instance that creates a ticket with a subject and body"
* "Create a tool that hits our internal enrichment API with a domain and returns company size, industry, and funding stage"
* "Add a tool that calls our CRM's REST API to look up a contact by email and return their account owner"
{/* TODO: Screenshot of Chat with the Custom Tool subagent active — showing it building a tool definition. */}
---
# Workflows (/chat/workflows)
Describe a workflow and Agent Serve builds it. Reference an existing one by name and Agent Serve edits it. No need to open the workflow builder — every change appears in the resource panel in real time.
## Creating Workflows [#creating-workflows]
Describe what the workflow should do — what triggers it, what it should do, which integrations it needs, and what it should return. Agent Serve builds it and opens the workflow in the resource panel.
* "Build a workflow that takes a URL, scrapes the page, summarizes it with Claude, and sends the summary to a Slack channel"
* "Create a workflow triggered by a webhook that extracts invoice data from a PDF and writes it to the billing table"
* "Build an outbound workflow: take a company name and domain, enrich it with firmographic data, score the fit, and draft a personalized cold email"
* "Create a lead enrichment workflow that takes an email from a form submission, looks up the company, and writes the enriched record to the leads table"
* "Build a customer onboarding workflow: when a new user signs up, send a welcome email, create a HubSpot contact, and post a notification to #new-customers in Slack"
## Editing Workflows [#editing-workflows]
{/* TODO: Screenshot of Chat with the Edit subagent active and a change applied to an open workflow — e.g., a new block added or a configuration updated, visible on the workflow in the resource panel. */}
Open an existing workflow with `@workflow-name` or the **+** menu, then describe the change. Agent Serve reads the current structure before modifying it — you don't need to explain what already exists.
* "Add a condition that routes to a different branch if the confidence score is below 0.7"
* "Replace the GPT-4o model with Claude Opus 4.6 on the summarizer block"
* "Add a Slack notification at the end that includes the output"
## Running Workflows [#running-workflows]
Ask Agent Serve to run a workflow and it handles the execution:
* "Run the data sync workflow"
* "Run the invoice processor with this PDF \[attach file]"
* "Test the lead scoring workflow with these inputs: name=Acme, score=0.4"
Execution streams back to the chat. The workflow in the resource panel shows live block-by-block state.
## Reading Logs [#reading-logs]
Agent Serve can retrieve and interpret execution logs for any workflow in the workspace:
* "Show me the last 10 runs of the pipeline workflow"
* "Why did the invoice workflow fail yesterday?"
* "What did the extractor block return in the most recent run?"
Logs include per-block execution state, outputs, errors, and timing.
## Debugging [#debugging]
When a workflow fails, tell Agent Serve to debug it:
* "Debug the last failed run of the content pipeline"
* "The summarizer block is returning empty output — figure out why"
Agent Serve reads the failure logs, identifies the cause, applies a fix, and can re-run to confirm.
{/* TODO: Screenshot of the Debug subagent section in Chat showing it reading logs and applying a fix. */}
## Deploying [#deploying]
Agent Serve can deploy a workflow as any of the three deployment types:
| Deployment type | What it creates |
| --------------- | -------------------------------------------------------------------------------------- |
| **API** | A REST endpoint at `https://agentserve.valnoraelric.com/api/v2/workflows/{id}/execute` |
| **Chat** | A hosted conversational interface with a shareable URL |
| **MCP tool** | An MCP server that exposes the workflow as a tool |
Ask: "Deploy the invoice workflow as an API and generate an API key."
Agent Serve can also roll back: "Revert the billing workflow to the version from last Tuesday."
See [API Deployment](/workflows/deployment/api) and [Chat Deployment](/workflows/deployment/chat) for full details on each deployment type.
## Organizing Workflows [#organizing-workflows]
Agent Serve can create and manage folders to keep your workspace organized.
**Folders:**
* "Create a folder called 'Data Pipelines'"
* "Move the invoice workflow into the billing folder"
* "Move the billing folder inside the finance folder"
* "Delete the old-experiments folder"
**Renaming and moving:**
* "Rename the 'test\_v2' workflow to 'lead-scorer'"
* "Move the summarizer workflow to the research folder"
{/* TODO: Screenshot showing Agent Serve confirming a folder or workflow organization action — e.g., a message confirming "Moved 'invoice-processor' into 'billing' folder" with the resource panel showing the folder open. */}
## Workflow Variables [#workflow-variables]
Agent Serve can set global variables on a workflow — values accessible across all blocks in that workflow at runtime:
* "Set the API\_ENDPOINT variable on the sync workflow to '[https://api.example.com/v2](https://api.example.com/v2)'"
* "Update the MAX\_RETRIES variable on the pipeline workflow to 5"
Variables set this way are available via `` syntax inside any block in the workflow.
## Deleting Workflows [#deleting-workflows]
* "Delete the old\_api\_prototype workflow"
* "Delete all workflows in the deprecated folder"
---
# AI in dashboards (/dashboards/ai)
## Describe with AI [#describe-with-ai]
In the builder, **Add panel → Describe with AI** turns a plain-English request — "monthly revenue by channel as a stacked bar" — into panel proposals. You choose **Data the AI may use**, the **Time range**, and **How many proposals**. Each proposal shows what data it reads before you add it.
## Change with AI [#change-with-ai]
On a panel's **AI** tab, describe a change (for example, "make it a line chart by month") and the panel is updated. The dashboard must be saved first; otherwise you see **Save the dashboard first to change panels with AI.**
## Explain this chart [#explain-this-chart]
**Explain this chart** in a panel's menu writes a short summary of what the panel shows and what stands out.
## Ask about this dashboard [#ask-about-this-dashboard]
**Ask about this dashboard** opens a chat about the dashboard, so you can ask follow-up questions about its numbers.
## AI filters [#ai-filters]
On the **Filters** tab, **Ask AI to add or change filters…** adds or edits filters from a description. See [Filters](/dashboards/filters-and-parameters).
AI features need a model configured for your deployment. If none is set up, you will see a message such as **AI summary is not configured on this server.**
---
# Formulas and calculations (/dashboards/calculations)
## Formulas [#formulas]
The **Formulas** tab in the builder holds dashboard-wide formulas. Each has a **Name**, a **Formula**, and **Show as** — **Number**, **Money**, or **Percent**. Formulas can use [parameters](/dashboards/filters-and-parameters#parameters), so a viewer can change an assumption and see every formula update.
For numbers your whole team should agree on, define them once as [metrics](/data/metrics) instead, and use the metric in any dashboard.
## Calculated fields [#calculated-fields]
A **calculated field** adds a new column to one panel's data, computed from other columns. Add one on the panel's **Data** tab under **Calculated fields** with **Add calculated field**.
## Comparisons [#comparisons]
On a panel's **Data** tab, the **Comparison** section sets **Compare with** to **None**, **Previous period**, or **Same period last year**. KPI cards and charts then show the change against that period.
## Combine sources [#combine-sources]
**Combine sources** on the **Data** tab opens the blend builder. **Add source** brings in another table or dataset, and **Shared keys** says which columns match between them — for example, a customer ID in both. The panel then shows **Blended data**, and **View data** shows which source each column came from.
---
# Call Compliance (/dashboards/call-compliance)
**Call Compliance** checks recorded conversations — sales calls, collections calls, support chats — against a checklist you define, and scores each one. It lives inside a dashboard as a **Transcript upload** panel, so the transcripts, the scores, and the charts about them sit together.
Create a dashboard from the **Call Compliance** template to get a transcript panel plus compliance KPIs and charts, or add a **Transcript upload** panel to any dashboard. (The old **Conversations** page now opens your Call Compliance dashboard.)
## Upload transcripts [#upload-transcripts]
Click **Upload transcripts** in the panel. You can bring transcripts in four ways:
* **Upload files** — `.txt`, `.vtt`, `.srt`, `.docx`, `.pdf`, `.json`, or `.md`. Uploaded files are also saved to a **Transcripts** folder in the [Library](/files).
* **Paste text** — write each line as `Speaker: text`, and separate transcripts with a line of `===`.
* **CSV** — a `.csv` or `.tsv` file with one conversation per row, and the dialogue in a column named `transcript`, `conversation`, or `text`.
* **Library folder** — pick transcripts already in the Library.
Click **Preview** to check what will be imported, then **Import**.
Limits: up to 15 MB per file, 200 files and 100 MB per upload, and 1,000 conversations per import. Very long transcripts (over 60,000 characters) are trimmed in the middle before analysis.
Each transcript shows **Waiting**, **Processing**, **Done**, or **Failed**, then its score and status.
## Configure checks [#configure-checks]
Open the panel's settings. Under **Transcripts**, choose the **Conversations table** (or **Upload to a new table**) and how many rows to **Show at most** (default 8, up to 50). Under **Compliance checks**, click **Configure checks** to open a four-step wizard:
1. **Checklist.** **Start from a checklist** — **Financial services sales calls**, **Collections**, **Healthcare**, or **Support QA** — or build your own. **Add a custom check** with a name, a severity (**Low**, **Medium**, **High**, or **Critical**), and **What to look for**. Up to 25 checks. A failed **Critical** check counts as a critical breach.
2. **Analytics.** Pick the **Columns to analyze** (transcript, agent, channel), add up to 10 custom fields (text, number, or yes/no), and give **Context for the reviewer**.
3. **Test.** **Test on sample transcripts** runs your checks and analytics on up to three transcripts from the table and shows the full result for each. Nothing is saved. Use **Run test again** after changing a check.
4. **Apply.** Choose what to run now — **Every row now**, **Rows not analyzed yet**, or **Do not run now** — and **Most rows per run** (default 500, up to 5,000). Tick **Analyze new rows automatically** to score new transcripts as they arrive. Click **Apply and run**.
## The compliance report [#the-compliance-report]
Click a transcript to open its **Compliance report**. It shows:
* The overall status (**Pass**, **Fail**, or a critical breach), the compliance **Score**, and a **QA** score.
* Every check with **Pass**, **Fail**, or **N/A**, its severity, the reason, and a quote from the transcript as evidence. A quote that could not be matched word for word is marked **Not found verbatim**.
* Insights: sentiment, intent, topics, objections, competitors, next steps, escalation risk, and how much the agent talked, plus your custom fields.
Scores and findings come from an AI model reading the transcript. Use the evidence quotes to check each finding before acting on it. Transcripts are never shown on viewer links.
---
# Exploring dashboards (/dashboards/exploring)
## Drill down and see rows [#drill-down-and-see-rows]
Click a point on a chart to open its menu:
* **Drill into** the next level of a hierarchy (for example, from region to country). **Drill up** goes back.
* **Filter dashboard by this** applies the clicked value as a filter.
* **See rows** opens the rows behind that point.
* **Go to** another view of the dashboard, if the panel links to one.
* **Annotate this point**.
From a panel's menu, **See rows** (or **See all rows**) opens every row behind the panel in a sheet you can search (**Search rows…**) and download with **Export CSV**. **View data** shows the query result and, for [blended](/dashboards/calculations#combine-sources) panels, where each column came from.
Rows always respect [row security](/dashboards/governance#row-security).
## Bookmarks [#bookmarks]
A bookmark saves the current filters and view so you can come back to them. Open **Bookmarks** in the header and use **Save bookmark**, **Rename bookmark**, and **Delete bookmark**. Each bookmark is **Only me** or **Everyone**, and one can be the default. Bookmarks are grouped into **Shared with everyone** and **Personal**.
## Annotations and notes [#annotations-and-notes]
Mark what happened at a point in time — a price change, an outage, a campaign launch. Use **Annotate this point** on a chart point, or **Annotate chart** from the panel menu. Type the note (**What happened here?**) and pick a colour. **Notes** in the panel menu holds longer notes about the panel. Annotations can also be managed on the panel's **Interactions** tab.
## Explain this chart [#explain-this-chart]
**Explain this chart** in a panel's menu writes a short plain-English summary of what the chart shows. See [AI in dashboards](/dashboards/ai).
## Explore a personal version [#explore-a-personal-version]
If you can view a dashboard but not edit it, click **Explore**. You get a personal layer on top of the shared dashboard where you can change panels, filters, and layout.
* **Save as my view** keeps your version. The button then reads **Edit my version**.
* **Submit to owner** sends your changes to the dashboard owner, with a **Note for the owner**.
* **Save as new dashboard** copies your version into a dashboard of your own.
* **Switch to shared dashboard** goes back to the shared version, and **Reset to the shared dashboard?** discards your changes.
Your version is labelled **Only you**, **Waiting for the owner**, **Accepted by the owner**, or **Not accepted**. Owners see **Review personal versions**, where they can accept or **Reject** submitted changes.
---
# Filters and parameters (/dashboards/filters-and-parameters)
## Filters [#filters]
Filters sit above the panels and narrow the data every connected panel shows. Add and arrange them on the **Filters** tab of the builder. Filter types:
| Type | Use it for |
| ---------------- | ------------------------------------------------------------ |
| **Select** | Pick one or more values from a list |
| **Date range** | A period, such as last 30 days or a custom range |
| **Text** | Free-text matching |
| **Number range** | A minimum and maximum |
| **Boolean** | Yes or no |
| **Condition** | Several conditions combined with AND or OR |
| **Top N** | Keep only the top (or bottom) N items by a measure |
| **Measure** | Filter on an aggregated value |
| **Hierarchy** | Pick a level in a hierarchy, such as region → country → city |
You can also type a request into **Ask AI to add or change filters…** on the Filters tab.
**Viewers apply filters with** decides how changes take effect for viewers: immediately, or batched behind an **Apply filters** button that shows how many changes are waiting. A selections bar shows what is applied and can clear everything at once.
Filters respect [row security](/dashboards/governance#row-security): a filter only ever narrows what a person is already allowed to see.
## Parameters [#parameters]
A **parameter** is a value a viewer can change that panels and formulas use — a target, a currency, an assumption such as a discount rate. Add one on the **Parameters** tab with **Add parameter**, and save it with **Save parameter**. Kinds include **Select (choices)**, **Currency**, **Date**, **Text**, and assumption values such as amounts and percentages.
Choose which panels respond to a parameter on each panel's **Parameters** tab, and use parameters inside [formulas](/dashboards/calculations#formulas).
## Scenarios [#scenarios]
Dashboards with parameters can have saved **scenarios** — named sets of parameter values such as Base, Best, and Worst. Switch scenarios from the **Scenario** picker in the header, and save the current values with **Save scenario**, choosing **Only me** or **Everyone** under **Who can see it**. See [Scenario models](/dashboards/scenarios).
---
# Row security and version history (/dashboards/governance)
Both settings live on the **Settings** tab of the dashboard builder. Row security can also be set on a table's **Share** sheet, so it applies everywhere that table is used.
## Row security [#row-security]
**Row security** decides which rows each person sees. It is enforced on every chart, KPI, **See rows**, export, scheduled report, and viewer link — and it narrows every viewer link further.
A policy has:
* **Name**
* **Applies to** — **A person**, **A team**, or **Everyone (except admins)**
* **Rows they see** — one or more conditions (column, operator, value). All conditions must hold. Values can use `{{user.email}}`, `{{user.id}}`, `{{user.name}}`, and `{{user.emailDomain}}` to match the person who is reading, and list values are separated by commas.
* **Status** — enforced or not
Use **Add policy** to create one and **Preview as** to check what a particular member would see before you save.
On a table, the **Share table** sheet also lets you choose who can see the table at all (**Everyone in the team space** or **Only people you choose**) and restrict individual columns to named people and teams.
## Version history [#version-history]
Every save is kept as a version — up to the last 100. Open **Version history** in Settings and pick a version to see what changed: panels, filters, and views **Added**, **Removed**, or **Changed**. Switch between **Changes in this version** and **Compared with now**, then click **Restore this version** to bring it back. Restoring is itself saved as a new version, so you can always undo it.
---
# Dashboards (/dashboards)
A **dashboard** is a page of panels — charts, KPI cards, tables, pivots, maps, and text — built on your [tables](/tables), [datasets](/data/data-sources), and [metrics](/data/metrics). Open **Team spaces → Dashboards** to see them.
## The dashboards list [#the-dashboards-list]
* **New dashboard** starts an empty dashboard. **Start from a template** opens the template gallery, and a **Recommended for you** strip suggests templates that fit your data.
* Tabs: **All dashboards**, **My dashboards**, and **Shared with me**. A **Certified** filter shows only [certified](/dashboards/sharing-and-reports#certification) dashboards.
* Sort by **Last updated**, **Date created**, or **Name**, and switch between list and grid views.
* A row's menu has **Open** and **Delete dashboard**. Deleting removes its panels and layout permanently.
## Templates [#templates]
Templates build a dashboard on **your own tables**. You pick which table fits each part. The gallery includes **Executive overview**, **Revenue trend**, **Pipeline tracking**, **Product performance**, **Customer segmentation**, **Operational metrics**, **Geographic analysis**, **Support tickets**, and **Call Compliance**.
## Viewing, editing, and exploring [#viewing-editing-and-exploring]
A dashboard opens in view mode. Across the top are the title, any [certification](/dashboards/sharing-and-reports#certification) badge, **Bookmarks**, **Share**, a **…** menu, and **Edit** if you can edit it. Dashboards can have several **views**, shown as tabs under the description.
* **Edit** opens the builder. A rail on the side has six tabs: **Build**, **Filters**, **Parameters**, **Formulas**, **Themes**, and **Settings**. Use **Undo**, **Redo**, and **Preview** while you work, then **Save** and **Done** — or **Discard** to throw away your changes.
* **Explore** is for people who can view but not edit. It creates a personal version you can change and save without touching the shared dashboard. See [Exploring dashboards](/dashboards/exploring#explore-a-personal-version).
The **…** menu has **Edit details…**, **Duplicate**, **Export…**, and **Explore a personal version** (or **Edit my personal version** once you have one).
## Limits [#limits]
A dashboard can have up to 120 panels, 24 filters, 20 views, and 16 parameters, laid out on a 12-column grid.
---
# Panels (/dashboards/panels)
In the builder, click **Add panel** to open the panel library. It is grouped into **KPIs**, **Charts**, **Data & tables**, **Maps & geo**, and **Cards & text**, with extra sections for **For you** suggestions, **Describe with AI**, **Templates**, and **Connect your data**.
## Panel types [#panel-types]
| Group | Panels |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **KPIs** | **KPI card** and **Gauge** — a single number, with optional comparison and target |
| **Charts** | Line, Bar, Area, Horizontal bar, Stacked bar, 100% stacked bar, Combo, Dual axis, Donut, Pie, Funnel, Scatter plot, Bubble, Heatmap, Treemap, Sunburst, Radar, Sankey, Waterfall, Box plot, Histogram, and Gauge chart |
| **Data & tables** | **Table** and **Pivot table**. A pivot can be exported to Excel (XLSX). |
| **Maps & geo** | Map, Bubble map, Cluster map, Heat map, and Region map |
| **Cards & text** | **Cards** (a feed of rows, useful for alerts and recommendations), **Transcript upload** (see [Call Compliance](/dashboards/call-compliance)), **Text** (Markdown), and **Section header** |
## The inspector [#the-inspector]
Select a panel to open its inspector. Data panels have these tabs:
| Tab | What you set |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Data** | The source, fields, aggregation, sorting, [calculated fields](/dashboards/calculations#calculated-fields), [comparisons](/dashboards/calculations#comparisons), and [blended sources](/dashboards/calculations#combine-sources) |
| **Parameters** | Which [parameters](/dashboards/filters-and-parameters#parameters) the panel responds to |
| **Logic** | How the value is calculated — row inclusion, transforms, null and error handling, state and colour rules, whether it ignores dashboard filters, and its provenance (why it has its value) |
| **Appearance** | Chart type, colours, labels, number formats, and layout |
| **Interactions** | What clicking does — drill, filter the dashboard, go to a view — and [annotations](/dashboards/exploring#annotations) |
| **AI** | **Change with AI** — describe a change in plain words. See [AI](/dashboards/ai). |
| **Access** | Who can see the panel |
Text and section panels only have **Appearance** and **Access**.
## Per-panel actions [#per-panel-actions]
Each panel's menu offers actions such as **See rows**, **View data**, **Explain this chart**, **Notes**, **Annotate chart**, **Export PNG**, **Download CSV**, and, on KPI cards, **Alert me when…**. See [Exploring dashboards](/dashboards/exploring) and [Sharing and reports](/dashboards/sharing-and-reports).
---
# Scenario models (/dashboards/scenarios)
A **scenario model** is a dashboard where viewers change assumptions — deal size, discount, term, cost to serve — and every number and chart updates. The demo workspace includes a **Deal Simulator** built this way: assumptions go in, and ARR, total contract value, margin, NPV, IRR, and payback come out, with scenario comparison, sensitivity, and goal seek.
## What you can use today [#what-you-can-use-today]
* **[Parameters](/dashboards/filters-and-parameters#parameters)** hold the assumptions viewers change.
* **[Formulas](/dashboards/calculations#formulas)** compute the outputs from those parameters.
* **Scenarios** — the **Scenario** picker in the header switches between saved sets of values such as **Base**, **Best**, and **Worst**. **Save scenario** stores the current values for **Only me** or **Everyone**.
* KPI cards and charts show the results.
The dedicated input, form, and scenario panels used in the demo Deal Simulator (assumption inputs, a "log this deal" form that writes back to a table, scenario comparison, sensitivity, and goal seek) can't yet be added or configured from the panel library. They appear in dashboards created from the demo data. Build your own what-if dashboards with parameters, formulas, and scenarios.
---
# Sharing and reports (/dashboards/sharing-and-reports)
## Share with your team [#share-with-your-team]
Open **Share** in the dashboard header (it reads **Restricted** when access is limited) and choose **Share access** to decide which people and teams can view or edit. Nobody gets more than their team-space role allows, and [row security](/dashboards/governance#row-security) still applies to everyone.
## Viewer links [#viewer-links]
In **Share access**, **Publish a viewer link** creates a link for people outside the workspace. Choose the audience:
* **Approved email addresses**
* **Anyone with a password** — the password must be at least 15 characters
* **Anyone with the link**
You can also set an expiry date, **Lock current filters** (or **Clear locks**) so viewers can't change them, **Allow data export**, and **Replace link when saving** so a new save issues a new link. Click **Publish link**, or **Save publishing settings** to update an existing one.
A viewer link only exposes the columns the dashboard actually uses. Columns that no panel, filter, sort, or calculation needs are removed from the data before it is sent, and row security narrows each link further. Transcript panels are never shown on viewer links.
## Export [#export]
From the **Share** menu:
* **Export PDF…** — choose the pages, paper size (including US Letter), orientation, theme (**Dashboard theme** or **Print (white paper, high contrast)**), and whether to include the applied filters.
* **Export PNG** — one view at a time.
* **Print…**
From a panel's menu, **Export PNG** saves one panel and **Download CSV** saves its data, one panel at a time. Pivot tables can also export to XLSX.
## Scheduled reports [#scheduled-reports]
**Subscribe…** emails the dashboard on a schedule. Set:
* **Frequency** — **Daily**, **Weekly**, or **Monthly**, and the **Time**
* **Format** — **PDF**, **PNG image**, or **Link only**. PDF and PNG are unavailable when data export isn't allowed.
* **Subject**, **Recipients**, **Outside addresses**, the **View** to send, and a **Message**
Outside addresses are limited to the domains your admins allow. Manage everything you have set up in **Manage reports & alerts**, which also shows a delivery log.
## KPI alerts [#kpi-alerts]
On a KPI card, **Alert me when…** sets an alert:
* **Condition** — **Goes above**, **Drops below**, or **Changes by** a percentage
* **Check** — **Every 15 minutes**, **Every hour**, **Every 6 hours**, **Once a day**, or **Once a week**
* **Notify** — **Email me**, **In-app** (always on, shown on Monitors), and **Slack**
After saving, **View in Monitors** opens the alert alongside your other [monitors](/data/monitors). **New KPI alert…** in the Share menu starts one from the header.
The Slack option needs your administrator to set a workspace alert webhook. Until then it is disabled with a note saying so.
## Certification [#certification]
**Certify** marks a dashboard as trusted for everyone ("Mark this dashboard as trusted for everyone"). Certified dashboards show a badge and can be filtered on the dashboards list. Any edit removes the certification — saving a certified dashboard asks **Save and remove certification?** — so use **Re-certify** after reviewing changes, or **Withdraw certification?** to remove it.
---
# Themes (/dashboards/themes)
Open the **Themes** tab in the builder to style the whole dashboard.
## Presets [#presets]
Start from a preset: **Default**, **Midnight**, **Slate**, **Ocean**, **Forest**, **Sunset**, **High contrast**, **Colour-blind safe**, or **Print**.
## Settings [#settings]
Fine-tune the theme with:
* **Chart palette** — the series colours
* **Colours** — backgrounds, text, and accents
* **Cards** — panel background and border
* **Corners** and **Spacing**
* **Panel title size** and **Panel title weight**
* **Numbers** — how values are formatted
Themes have light and dark variants, so a dashboard follows each viewer's appearance setting. The **Print** preset and PDF export's print option use white paper and high contrast.
---
# Agent Serve Desktop (/desktop)
Agent Serve Desktop is the macOS app for your Agent Serve workspace. Everything the web app does, it does — plus the things a browser tab cannot: a built-in browser and terminal, read-only access to folders you pick, and native notifications when a task finishes.
## Download [#download]
**[Download Agent Serve Desktop for macOS](https://agentserve.valnoraelric.com/api/desktop/update/download)**
One universal build runs natively on both Apple Silicon and Intel Macs. It is signed and notarized by Agent Serve, so Gatekeeper accepts it with no override.
That link is never version-pinned — it is a redirect resolved at request time, so it always lands on the newest release. Bookmark it, share it, or put it in an MDM policy; it stays correct.
It resolves against agentserve.valnoraelric.com. Every deployment serves the same endpoint on its own origin, so if you self-host use yours — `https://agentserve.example.com/api/desktop/update/download` — to get the build your deployment offers.
To install a specific version instead of the newest one, pick it from [the releases page](https://github.com/Suryanandx/agent-serve-platform/releases) and download that release's `AgentServe--universal.dmg`.
## Install [#install]
### Open the disk image and drag Agent Serve to Applications [#open-the-disk-image-and-drag-agent-serve-to-applications]
Install to `/Applications`. macOS App Translocation runs an app from a randomized read-only path when it is launched from Downloads, which silently breaks auto-updates.
### Sign in [#sign-in]
Launch Agent Serve and sign in as you normally would.
Google, Microsoft, and SSO sign-ins finish in your default browser — those providers refuse to render inside an embedded browser. Agent Serve opens the page, you approve, and the browser hands the session back to the app. The app gets its own session, so signing out of one surface does not sign out the other.
### Point it at your deployment, if you self-host [#point-it-at-your-deployment-if-you-self-host]
Fresh installs open agentserve.valnoraelric.com. To use your own deployment, choose **Agent Serve → Server…** in the menu bar and enter its URL. See [Desktop App on Your Deployment](/platform/self-hosting/desktop) for what changes when you switch.
## What the desktop app adds [#what-the-desktop-app-adds]
* **A built-in browser.** A real browser inside the app, with its own tabs, saved passwords, and sessions. Chat can drive it — sign in once and your agents work on the sites you are already signed into.
* **A built-in terminal.** Real shell sessions in a panel next to Chat, with tmux and shell integration, that Chat can run commands in.
* **Local folder access.** When a task needs a folder on your Mac, Chat offers to open the native folder picker. The grant is read-only, scoped to the folder you picked, and revocable.
* **Notifications.** A native notification when a Chat task finishes. Clicking it opens that chat. Tasks that end in an error, or that have another message queued behind them, do not notify.
* **Control Center.** A menu-bar icon with your recent chats, so Agent Serve is one click away from any app.
* **Launch at login.** Agent Serve starts with your Mac, and however you launch it, it opens where you left off.
Both the browser and the terminal are capabilities you grant, not defaults you are stuck with — each has a single switch in settings that turns it off entirely.
**What a folder grant does and does not do.** Granting a folder does not copy or upload it, and an agent cannot attach or stage a file from it — that stays your deliberate act. But when an agent reads or searches inside the grant, what it reads is a tool result, and tool results go to your Agent Serve server and to the model like anything else in the conversation. The file stays on your Mac; what an agent reads out of it does not.
## Keyboard shortcuts [#keyboard-shortcuts]
These are the app's own shortcuts. The [workflow editor and table shortcuts](/keyboard-shortcuts) work the same in the app as in the browser.
| Shortcut | Action |
| ----------------------- | --------------------------------- |
| `Cmd` + `K` | Search |
| `Cmd` + `B` | Toggle the sidebar |
| `Cmd` + `N` | New chat |
| `Cmd` + `Shift` + `N` | New window |
| `Cmd` + `,` | Settings |
| `Cmd` + `[` | Back |
| `Cmd` + `R` | Reload |
| `Cmd` + `0` / `+` / `-` | Reset, increase, or decrease zoom |
With the built-in browser or terminal focused, the tab shortcuts act on its tabs rather than on the window:
| Shortcut | Action |
| ----------------------------------------- | ------------------------------------ |
| `Cmd` + `T` | New tab |
| `Cmd` + `W` | Close tab |
| `Cmd` + `Shift` + `T` | Reopen the last closed tab |
| `Ctrl` + `Tab` / `Ctrl` + `Shift` + `Tab` | Next / previous tab |
| `Cmd` + `1`–`8` | Jump to that tab |
| `Cmd` + `9` | Jump to the last tab |
| `Cmd` + `L` | Focus the address bar (browser only) |
| `Cmd` + `F` | Find on the page (browser only) |
## Settings [#settings]
The desktop app adds three sections under **Settings → Account**. They appear only when you are running the app, and they apply to this Mac rather than to your account.
### Desktop [#desktop]
* **Launch Agent Serve at login**
* **Show Agent Serve in Control Center** — the menu-bar icon
* **Automatically download updates**
* **Enable desktop notifications**, with **Play notification sounds** and **Notify only when Agent Serve isn't focused**
It also shows the installed version, and the version waiting to be applied when an update is ready.
### Browser [#browser]
* **Let Chat browse the web** — the master switch for the built-in browser
* **Search suggestions**, **Theme**, **Default zoom**, and **Download location**
* **Browsing data** — clear cookies, site data, and cached images and files
### Terminal [#terminal]
* **Let Chat run commands** — the master switch for the built-in terminal
* **Theme** and **Default zoom**
The menu bar carries the rest: **Agent Serve → Settings…** (`Cmd` + `,`), **Server…** to change deployments, **Check for Updates…**, and **Sign Out**.
## Updates [#updates]
Agent Serve checks the deployment it is pointed at rather than a global feed. How it applies what it finds depends on how the app was installed. Nothing is ever forced mid-session either way.
**Installed in `/Applications`, signed by Agent Serve** — what the download link above gives you. The app replaces itself. With **Automatically download updates** on, it downloads in the background and offers to restart; choose **Later** and the update applies the next time you quit. With it off, nothing downloads until you ask: **Agent Serve → Check for Updates…** reports the available version and waits for you to choose **Download**.
**Anywhere else** — outside `/Applications`, or a build not signed with a Developer ID. The app cannot replace itself, so it offers you the installer to download and swap in by hand.
Updates come from the deployment you are connected to, so a self-hosted install controls which build its own users are offered. That control depends on the feed staying reachable: if it is not, a self-updating stable build falls back to Agent Serve's public GitHub releases rather than stalling. See [Desktop App on Your Deployment](/platform/self-hosting/desktop).
## Requirements [#requirements]
* **macOS 12 Monterey or later**, on Apple Silicon or Intel.
* **Outbound access to your Agent Serve deployment**, and to `github.com`, which is where installers and updates are downloaded from.
* **If you self-host**, your Agent Serve server needs its own outbound access to both `api.github.com`, which is what resolves *which* release to offer, and `github.com`. An allowlist carrying only `github.com` leaves the download endpoint answering `502`.
* **A system-trusted TLS certificate**, if you self-host. The app rejects certificate errors outright and offers no override, so a private CA must be installed in the macOS keychain.
The desktop app is macOS-only today. The web app works in any browser on any platform, and your account, workspaces, and workflows are the same either way.
---
# Data sources (/data/data-sources)
Open **Team spaces → Data → Data sources** ("Connect your data"). The page shows **Connected sources**, **Datasets**, **Rows synced**, and **Sync errors**, then the **Available sources** catalog, your **Connected sources**, and your **Datasets**.
## Available connectors [#available-connectors]
Filter the catalog by **All**, **Warehouses**, **Databases**, **Apps & BI**, or **Files**.
| Group | Connectors |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Warehouses** | Databricks, Snowflake, BigQuery |
| **Databases** | PostgreSQL, MySQL, SQL Server |
| **Apps & BI** | Salesforce (CRM data through SOQL, Salesforce's query language), Qlik (Qlik Cloud or Qlik Sense apps) |
| **Files** | Excel (workbooks and CSV files), Transcripts (calls, chats, and meeting notes for [Call Compliance](/dashboards/call-compliance)) |
## Add a source [#add-a-source]
Click a connector, or **Connect a source**, to open **New data source**. Pick the **Engine**, give it a **Name** and an optional **Description**, then fill in the connector's fields.
Credential fields take the **name** of a workspace secret (for example `POSTGRES_HOST`), not the secret value itself. Store the value in **Settings → Secrets** first. See [Secrets](/platform/credentials).
**Qlik** asks for the deployment — **Qlik Cloud** or **Qlik Sense Enterprise (client-managed)** — and the authentication method: **API key**, **OAuth client credentials (machine-to-machine)**, **JWT via a virtual proxy**, or **Header authentication via a virtual proxy**. **Excel** needs no credentials.
A connected source's menu has **Test connection**, **Add dataset**, **Edit source**, and **Delete source**.
## Datasets [#datasets]
A **dataset** is the data you pull from a source. Click **New dataset**, choose the **Data source** and a **Name**, then set:
* **Execution** — **Materialized (scheduled)** copies the data into Agent Serve on a schedule; **Live (per request)** queries the source each time it is used.
* **Refresh** — **Manual only**, **Every 15 minutes**, **Hourly**, **Every 6 hours**, or **Daily**.
* **Row cap** — the most rows to keep.
* **Query** — the SQL to run.
For **Qlik**, a dataset extracts **Fields**, a **Sheet object**, or (Qlik Cloud only) a **Data file**. For **Excel**, upload a workbook or pick one from the Library (`.xlsx`, `.xls`, or `.csv`, up to 25 MB), then choose the **Sheet**, the **Range** (**Auto-detect** or an A1 range such as `A1:F200`), and the **Header row** (**Detect**, **First row**, or **None**).
A dataset's menu has **Refresh now**, **Replace file…** (Excel only), **Version history**, **Edit dataset**, and **Delete dataset**. Each dataset shows **Synced**, **Syncing**, **Error**, or **Idle**, when it was last refreshed, and how many rows changed. **Version history** lists recent refreshes as **Refreshed**, **Scheduled**, or **File changed**.
Datasets can be used in [dashboards](/dashboards), [metrics](/data/metrics), and the [semantic model](/data/semantic-model).
---
# Data (/data)
The **Team spaces** tabs hold your team's shared data. Data flows in through **data sources**, lands in **datasets** and **tables**, and is given meaning by **metrics**, the **semantic model**, and **rules**. **Monitors** watch it and tell you when something changes. **Workbooks** are where you explore it in SQL or plain English before a result goes on a dashboard. [Dashboards](/dashboards), [Chat](/chat), and [Relays](/relays) all read from the same definitions, so a number means the same thing everywhere.
---
# Metrics (/data/metrics)
Open **Team spaces → Model → Metrics** ("Trusted metrics for every insight"). A **metric** is a named, reusable definition of a number. When a dashboard panel, Chat, or a Relay uses a metric, everyone gets the same calculation.
The page lists every metric with its type, description, owner, and when it was last updated. Filter by type and owner, search by name, or browse by the entity a metric belongs to.
## Create a metric [#create-a-metric]
Click **New metric** and fill in **Name**, an optional **Description**, the **Metric type**, the **Format** (**Number**, **Currency**, or **Percent**), and an optional **Unit**.
| Metric type | How it is calculated |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Aggregate** | One column of one table, with **Sum**, **Average**, **Min**, **Max**, **Count (rows)**, **Count distinct**, or **Median** |
| **Weighted** | A **Value column** and a **Weight column**, either as a **Weighted average** (Σ(value × weight) ÷ Σweight) or a **Weighted total** (Σ(value × weight)) |
| **Ratio** | One metric divided by another — a **Numerator metric** over a **Denominator metric** |
| **Calculated** | A **Formula** over named **Variables** |
Deleting a metric removes it from any dashboard panel that uses it, so the app warns you first.
Metrics can be watched by [monitors](/data/monitors) and given to [Relays](/relays/knowledge).
---
# Monitors and alerts (/data/monitors)
Open **Team spaces → Monitors**. The page counts your **Monitors**, how many are **Active**, how many are **Firing**, and how many alerts are **Open**. It has two tabs: **Watchers**, the list of monitors, and **Alerts**.
## Create a monitor [#create-a-monitor]
Click **New monitor** and choose what to watch:
* **Metric** — a metric threshold. Set **Fire when it** **falls below**, **is at or below**, **rises above**, **is at or above**, **equals**, or **differs from** a **Threshold**.
* **Rule** — a rule match. Choose the table it runs **Over**, and **Fire when at least this many rows match**.
Then set the **Monitor name**, how often to **Check** (**Every 5 minutes**, **Every 15 minutes**, **Hourly**, **Every 6 hours**, or **Daily**), and the **Severity** (**Info**, **Warning**, or **Critical**).
A monitor is **Active**, **Firing**, or **Paused**; you can turn it on or off from the list. Each alert it raises is **Open** until it is **Resolved**.
## KPI alerts from dashboards [#kpi-alerts-from-dashboards]
On a dashboard KPI card, **Alert me when…** creates an alert that also appears here. Filter the list by **Dashboard KPIs** to see them. See [KPI alerts](/dashboards/sharing-and-reports#kpi-alerts).
## Where alerts go [#where-alerts-go]
* **In the app** — every alert appears on the Monitors page and in [Notifications](/workspace#notifications).
* **Email** — KPI alerts can email you.
* **Slack** — when your administrator sets a workspace alert webhook, monitors post there.
A [Relay](/relays/setup#4-ongoing-work) can pick up work when a monitor fires — choose **When a monitor alerts** as a responsibility trigger.
---
# Rules (/data/rules)
Open **Team spaces → Model → Rules** ("Turn data into reliable decisions"). A **rule** is logic you define once and reuse — in dashboards, in [monitors](/data/monitors), and by [Relays](/relays/knowledge).
## Rule types [#rule-types]
| Type | Produces | Defined by |
| --------------------------- | ------------------------- | ----------------------------------------------------------------------- |
| **Flag (true / false)** | Yes or no for each record | A **Condition** |
| **Classification (labels)** | A label for each record | **Buckets**, each with a condition, plus an optional **Fallback label** |
| **Score (points)** | A number for each record | **Scoring terms** that add points when their condition holds |
Conditions compare a field with `=`, `≠`, `>`, `≥`, `<`, `≤`, **in**, **not in**, **between**, **contains**, **is empty**, or **is set**, and can be grouped.
Each rule shows as **Active** or **Archived**.
For example, a **Flag** rule "High-value account" might be `annual_revenue > 1,000,000`; a **Classification** rule might bucket customers into Gold, Silver, and Bronze; a **Score** rule might add points for each risk signal on a loan.
---
# Semantic model (/data/semantic-model)
Open **Team spaces → Model → Semantic model** ("Unified data. Clear context."). The **semantic model** describes your data in business terms: what a customer, an order, or a branch is, which table holds it, how tables connect, and what people call things. Chat, dashboards, and Relays use it to join data correctly and to understand questions in your team's own words.
The left side lists the model's parts with counts: **Entities**, **Relationships**, **Conformed dimensions**, **Dimension fields**, **Attributes**, **Measures**, and **Synonyms**. Select an entity to see its related entities as a diagram, with details on the right.
## Entities [#entities]
An **entity** is a business object backed by a table. Click **New entity** and set its **Name**, **Table**, **Synonyms** (comma-separated words people use for it, like "outlets" for branches), its **Attributes & dimensions**, **Measures**, and **Relationships**.
A measure can be a formula, for example `DIVIDE(SUM([revenue]), COUNT([order_id]))`.
## Relationships [#relationships]
A **relationship** says how two entities join. Set:
* **Key(s)** and **Cardinality** (such as one-to-many)
* **Join** — **Keep unmatched** or **Matches only**
* **Key matching** — **Exact match**, **Trim spaces**, or **Trim + ignore case**
A data check runs against the real tables and reports **Duplicate keys**, **Orphan keys**, and what the data says the cardinality actually is. Agent Serve can also suggest relationships, and verified ones are marked on the diagram.
## Conformed dimensions [#conformed-dimensions]
A **conformed dimension** is a dimension shared across entities — such as Market or Date — so filters line up across tables. Each has a **Type** (text or date), a grain for dates (day through year), and value maps that reconcile different spellings of the same value.
## Drafts and versions [#drafts-and-versions]
With model drafts on, you change the model in a private **draft** and publish it when it is ready. Everyone else keeps reading the **published** model until you do.
Model drafts are a preview that is turned on for your deployment. While they are off, edits save straight to the published model, and the model page shows only the **Model** view.
### Start a draft [#start-a-draft]
The banner at the top of the model page holds your draft. **Start draft** copies the published model. **Start from tables** picks up to 50 tables and proposes entities, measures (a row count and sums of numeric columns) and joins (from foreign keys and matching column names), which you then review.
Each person has at most one open draft per team space. Nobody else can see your draft.
Without a draft, saving a change publishes it straight away and records a version. A team space that requires a second approver refuses that save, so start a draft there.
### Draft mode [#draft-mode]
**Draft mode** decides what your own work reads while the draft is open. With it on, your semantic queries, dashboards, dashboard chat, [workbook](/data/workbooks) Semantic cells and the model page read your draft, so you can test a change on real panels before anyone else sees it. Turn it off to see the published model again.
Draft mode never affects anyone else. Work that runs for nobody in particular — workspace API keys, schedules, shared viewer links and Relays running unattended — always reads the published model.
### Views [#views]
The model page has five views:
| View | What it shows |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Model** | The visual editor: entities, relationships, measures and the rest of the model. |
| **Code** | The model as `agentserve.model.yaml`. Saving checks it against the model schema and writes your draft, which the visual editor then shows. |
| **Versions** | Every published version: who published it, when, and with what note. Compare a version with the one before it or with the current one, and **Roll back**. |
| **Compare** | Pick up to eight measures and run each one under your draft and under the published model, side by side. |
| **Reviews** | Workspace admins only. The publishing policy and the publish requests waiting on a decision. |
You can also work with the model from the terminal: `agentserve model pull` writes the published model or your draft to `agentserve.model.yaml`, and `agentserve model diff` shows what the file would change. See [agentserve model](/cli/model).
### Publish [#publish]
**Publish draft** asks for an optional note ("What changed and why") and publishes your draft as the next version. The draft then closes. **Discard draft** throws your draft away and leaves the published model unchanged.
An admin can set **Publishing approval** to **Require a second approver** under **Reviews**. Then **Publish draft** becomes **Send for approval**: your draft waits, locked, until a workspace admin other than you approves it, which publishes it as the next version, or rejects it, which hands it back to you to keep editing.
### Roll back [#roll-back]
**Roll back** on a version publishes that version's model again as a new version, so the history keeps every step. When publishing approval is required, only an admin can roll back.
### Who can do what [#who-can-do-what]
| Action | You need |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Read the published model | **Read** in the team space |
| Start, edit, publish or discard a draft; read versions; compare; roll back | **Write**, and a permission group that allows **Model Management** |
| Turn publishing approval on or off; approve or reject a request | **Admin**, and a permission group that allows **Model Management** |
---
# Workbooks (/data/workbooks)
A **workbook** is a page of cells for exploring your data. You write SQL against a [data source](/data/data-sources), ask questions in plain English, query the [semantic model](/data/semantic-model), chart the rows, and add notes. When a query is worth keeping, you save it as a dataset or put its chart on a [dashboard](/dashboards).
Open **Team spaces → Workbooks**. The list shows the team space's workbooks, most recently changed first. Click **New workbook** and give it a name.
Workbooks are a preview that is turned on for your deployment. If you do not see the **Workbooks** tab, it is not on yet, or your [permission group](/platform/enterprise/access-control) withholds it.
## Cells [#cells]
Add a cell with the buttons under the last cell. Move cells up or down, or delete them, from each cell's controls. A workbook holds up to 100 cells.
| Cell | What it does |
| ------------ | ------------------------------------------------------------------------------------------------------------------------- |
| **SQL** | One read-only query against a data source you pick. |
| **Ask** | A question in plain English. AI suggests SQL for it, and you accept, edit or dismiss the suggestion before anything runs. |
| **Semantic** | Measures, group-by dimensions, filters and a row limit, picked from the semantic model. No SQL. |
| **Chart** | A chart of the rows from a SQL, Ask or Semantic cell above it. Pick the chart type, the category and the measure. |
| **Text** | Notes in Markdown. |
Workbooks query SQL data sources: warehouses and databases. If none is connected yet, the workbook says so; add one under **Data sources**.
## Running a query [#running-a-query]
Press **Cmd+Enter** (Mac) or **Ctrl+Enter** to run a SQL or Ask cell. Every run goes through the same path as dataset queries: one read-only statement, the source's timeouts, and a row cap.
The cell shows the rows, the row count and how long the run took. **CSV** downloads the rows. A run keeps at most 1,000 rows and 2 MB with the cell. Stored results expire after seven days; the query stays, and the cell asks you to run it again.
**History** in the workbook header lists past runs: who ran which cell, against which source, whether it succeeded, how many rows it returned and how long it took. Every run is also in the [audit log](/platform/enterprise/audit-logs).
### Large scans [#large-scans]
Before a run, Agent Serve checks whether the query is likely to read a lot. On PostgreSQL it asks the database for its plan estimate. On other sources it reads the query for a missing `WHERE` or `LIMIT`, `SELECT *`, or tables joined with commas and no join condition. A large scan stops first and offers **Make it safer**, or **Run anyway**. Nothing is rewritten unless you accept the suggestion.
## AI help with SQL [#ai-help-with-sql]
The SQL editor highlights SQL, completes table and column names from the source's schema, and shows AI suggestions as faint text after a pause in typing. Press **Tab** to accept one.
Each query cell also offers:
* **Ask**: turn a question into SQL.
* **Explain**: describe in plain words what the query does.
* **Fix this error**: after a failed run, explain the error and suggest a corrected query.
* **Make it safer**: suggest a version that reads less, for example by adding a filter or a limit.
The AI sees the source's schema as you can see it, and the semantic model's names, synonyms and measures. It never sees row data. Every suggestion is only a suggestion: nothing runs or saves until you accept it and run the cell. AI calls are billed to the workspace like other model use.
## Saving and publishing [#saving-and-publishing]
From a SQL or Ask cell, **Save as dataset** creates a new dataset from the query, or adds it to an existing one. The dataset then appears under [Data sources](/data/data-sources), where dashboards, Chat and Relays can use it.
From a Chart cell, **Publish to dashboard** adds the chart as a panel on a new or existing dashboard. For a chart over a SQL or Ask cell, the panel reads a new dataset of that query. For a chart over a Semantic cell, the panel reads the semantic model directly, so each viewer sees only the rows their access allows.
## How workbooks fit with dashboards and the semantic model [#how-workbooks-fit-with-dashboards-and-the-semantic-model]
* A workbook is where you explore. A [dashboard](/dashboards) is where the team reads the result. Publish a chart when the question is settled.
* Semantic cells use the same measures and joins as dashboards and Chat, with the same table access and [row security](/dashboards/governance#row-security).
* If you have a [model draft](/data/semantic-model#drafts-and-versions) with draft mode on, your own Semantic cells and AI suggestions read your draft. Everyone else reads the published model.
## Sharing and permissions [#sharing-and-permissions]
A workbook is shared with the team space by default. A person who can manage it can open **Share** to limit it to chosen people and teams, with view, edit or manage access. A restricted workbook is hidden from everyone it is not shared with. Workspace admins always see every workbook.
| To | You need |
| ------------------------------------------------- | ------------------------------------------------------------ |
| Open a workbook and read its cells | **Read** in the team space, and view access to the workbook |
| Add, edit or run cells, or use AI help | **Write** in the team space, and edit access to the workbook |
| Delete a workbook or change who it is shared with | Manage access to the workbook |
Data source access applies to every cell, separately:
* Running a cell, reading its schema or asking AI about it needs use access to the cell's source. A cell's stored result is shown only to people who can use its source.
* **Save as dataset** needs edit access to the source, and your permission group must allow data management.
* **Publish to dashboard** needs your permission group to allow building dashboards.
A permission group that withholds **Workbooks** blocks every workbook action, including running queries and AI help. See [Access Control](/platform/enterprise/access-control#platform).
## Limits [#limits]
| Limit | Value |
| ------------------------- | ----------------- |
| Cells per workbook | 100 |
| SQL per cell | 20,000 characters |
| Question per Ask cell | 2,000 characters |
| Markdown per Text cell | 50,000 characters |
| Rows kept per run | 1,000 |
| Result size kept per run | 2 MB |
| How long a result is kept | 7 days |
---
# agentserve app (/developers/cli)
The `agentserve app` commands take an app from an empty folder to an install people use, and help you operate it afterwards. Sign in first with `agentserve login`; see [Authentication](/cli/authentication). Every flag of every command is in the generated [CLI reference](/cli/app).
## Choosing the app and workspace [#choosing-the-app-and-workspace]
Commands that act on an app that already exists pick it in this order:
1. `--app `, when you pass it.
2. The `name` in `./agentserve.app.yaml`, when you run the command in the app's folder.
The usual [global options](/cli/commands#global-options) apply: `-P, --profile` picks a login profile, `-w, --workspace` a workspace, and `--output` the output format. Every `app` command also takes `--json`, which prints one machine-readable JSON document instead of text.
## Exit codes [#exit-codes]
| Code | Meaning |
| ----- | ------------------------------------------------------------------------------------------------------------- |
| `0` | Success |
| `1` | The API refused or failed the request, or the command was used wrongly (an unknown flag, a missing argument) |
| `2` | A problem with the app on disk: no manifest, an invalid manifest, a failed build, or a bundle over its limits |
| `130` | You cancelled a prompt |
## agentserve app init [#agentserve-app-init]
```bash
agentserve app init [dir] [--template ] [--name ] [--title ] [--force]
agentserve app init --list
```
Creates an app from a template in `dir` (default: the current folder).
| Option | Description |
| ------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `--template ` | The template to copy. Defaults to `blank`. See [Templates](/developers/templates). |
| `--name ` | The manifest `name`. Defaults to the folder name, made into a valid name (lowercase letters, digits and hyphens). |
| `--title ` | The manifest `title`. |
| `--force` | Write into a folder that is not empty. Without it, `init` refuses. |
| `--list` | Print the templates and exit. |
`init` also copies the app SDK into `vendor/app-sdk`. Then install the dependencies:
```bash
cd my-app
bun install # or: npm install
```
## agentserve app dev [#agentserve-app-dev]
```bash
agentserve app dev [dir] [--port ] [--page ] [--build-command ] [--no-build] [--no-open]
```
Builds the app and serves it at `http://localhost:5175` in a local host page. The host page frames the app with the same sandbox and content security policy as the workspace, so what works in `dev` works when installed.
SDK calls are forwarded to your workspace through the API with your own CLI login. The app sees exactly what you can see, and the manifest still limits which tables, workflows and Relays it may name. When files change, `dev` rebuilds and reloads the frame; it ignores `dist/`, `node_modules/` and `vendor/`.
| Option | Description |
| ----------------------- | ------------------------------------------------------------ |
| `--port ` | Port to serve on. Defaults to `5175`. |
| `--page ` | The manifest page to open first. |
| `--build-command ` | Command that builds the app. |
| `--no-build` | Serve what is already built, without building or rebuilding. |
| `--no-open` | Do not open a browser. |
Differences from an installed app:
* `askChat` shows the prompt in the host page instead of opening Chat.
* `getViewer()` returns your profile's workspace, the manifest's name, version and permissions, and placeholders for the rest: `dev` for the user, app, install and version ids, no name or email, and the role `write`.
* `runTool` and `listTools` use the version of your app **installed** in the workspace, because code tools run only on Agent Serve. Your local `tools/` files are not used until you deploy, publish and install them. With no installed version that ships tools, both fail with `not_found` and a message saying how to install one.
* `assignRelayTask` and `queryModel` go through the same API endpoints a script would use, `POST /api/v2/relays/tasks` and `POST /api/v2/semantic-model/query`, as you.
`dev` needs an OAuth login or a personal API key: the model, Relay and code tool calls refuse a workspace API key.
## agentserve app deploy [#agentserve-app-deploy]
```bash
agentserve app deploy [dir] [--build-command ] [--no-build] [--json]
```
Runs the build, packs the app into a `.zip`, and uploads it as a **draft version**. The archive holds the manifest, the folders your pages, panels and Chat cards live in (such as `dist/`), the icon, the files `requires` names, and the [code tool](/developers/code-tools) files `tools` names. It prints the version and a link to the app's versions page in the workspace.
The first deploy of a new `name` creates the app. Each later deploy needs a new `version` in the manifest; deploying a version that already exists fails. Uploading needs the `write` role.
Archive limits:
| Limit | Value |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Archive size | 25 MB |
| Files | 500 |
| One file | 10 MB |
| Unpacked total | 50 MB |
| File types | Static files only: `html`, `htm`, `js`, `mjs`, `css`, `json`, `map`, `txt`, `md`, `yaml`, `yml`, `csv`, `svg`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `avif`, `ico`, `woff`, `woff2`, `ttf`, `otf`, `mp3`, `mp4`, `webm` |
Files the `tools` globs name may also be TypeScript, JavaScript or Python sources (`ts`, `mts`, `js`, `mjs`, `py`). They are stored with the version and never served to the app's frame. See [Code tools](/developers/code-tools).
To build on Agent Serve from a repository instead, see [Deploying from Git](/developers/git-deploys).
## agentserve app publish [#agentserve-app-publish]
```bash
agentserve app publish [--app ] [--rollout] [--json]
```
Makes a draft version installable. Needs the `admin` role. With `--rollout`, this workspace's install also moves to the version.
## agentserve app rollback [#agentserve-app-rollback]
```bash
agentserve app rollback [--app ] [--json]
```
Moves this workspace's install back to an earlier published version. Needs the `admin` role.
## agentserve app install [#agentserve-app-install]
```bash
agentserve app install [--app ] [--to-version ] [-w ]
```
Installs the highest published version, or the one `--to-version` names, in the workspace. It prints the tables, rules and Relays it created, the ones the workspace already had, and the ones it skipped with the reason. Needs the `admin` role.
Relays an app declares under `requires.relays` are created only by an install from the **Apps** page in the workspace. An install from the CLI reports them as skipped; create them in Relays, or install from the workspace.
## agentserve app logs [#agentserve-app-logs]
```bash
agentserve app logs [--app ] [--follow] [--interval ] [--json]
```
Prints the install's audit trail, newest last: version changes, secret changes and every write the app made. `--follow` keeps polling for new entries, every `--interval` seconds. Needs the `admin` role.
## agentserve app usage [#agentserve-app-usage]
```bash
agentserve app usage [--app ] [--json]
```
Prints this month's spend against the install's budget, broken down by source. Needs the `admin` role.
## agentserve app permissions [#agentserve-app-permissions]
```bash
agentserve app permissions [dir] [--app ] [--json]
```
Prints what the installed version may reach, next to the latest uploaded version and, in an app folder, the local manifest. Differences from the install are highlighted, so you can see what a new version would add or remove before you publish it.
## agentserve app secrets [#agentserve-app-secrets]
```bash
agentserve app secrets set [--value ]
agentserve app secrets list
agentserve app secrets delete
```
Manages the install's write-only secrets. Needs the `admin` role.
* `set` stores a value. `--value` takes the text itself, `@path` to read a file, or `@-` to read standard input. Without `--value`, it prompts for the value without showing what you type.
* `list` prints each secret's name and when it last changed, never its value.
* `delete` removes a secret.
See [Secrets](/developers/security#secrets) for the naming rules and what secrets are used for today.
## A release, end to end [#a-release-end-to-end]
```bash
# In the app folder, after raising version to 0.3.0
agentserve app deploy
agentserve app permissions # check what changes for admins
agentserve app publish 0.3.0 --rollout
agentserve app logs --follow
```
---
# Code tools (/developers/code-tools)
Some logic should give the same answer every time: a pricing check, a score, a routing rule. You want it in code you can test, not in a prompt. A **code tool** is a function you ship with your app. Agent Serve runs it on the server, in a sandbox, whenever your app, Chat, a Relay or a workflow calls it. An agent that calls a tool cannot talk it into a different answer.
## Files [#files]
A tool is two files in your repository, usually under `tools/`, sharing a name:
* `.tool.yaml` describes the tool: what it does, the input it takes and the output it returns.
* `.ts`, `.mts`, `.js`, `.mjs` or `.py` is the code.
Name them in the manifest with a glob or a list of globs:
```yaml title="agentserve.app.yaml"
tools: ./tools/*
```
The [deal desk template](/developers/templates#deal-desk) ships one tool, `score_offer`:
```yaml title="tools/score_offer.tool.yaml"
name: score_offer
description: >-
Scores a sales offer from 0 to 100 against the deal desk pricing policy and says
whether to approve, review or reject it, with the reasons.
effect: read
timeoutMs: 10000
input:
type: object
required: [discountPct, marginPct, termMonths, dealSize, tier]
properties:
discountPct: { type: number, minimum: 0, maximum: 100 }
marginPct: { type: number, minimum: -100, maximum: 100 }
termMonths: { type: number, minimum: 1, maximum: 120 }
dealSize: { type: number, minimum: 0 }
tier: { type: string, enum: [strategic, enterprise, standard] }
output:
type: object
required: [score, decision, reasons]
properties:
score: { type: integer, minimum: 0, maximum: 100 }
decision: { type: string, enum: [approve, review, reject] }
reasons: { type: array, items: { type: string } }
```
| Field | Required | Meaning |
| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | Yes | Lowercase letters, digits and underscores, starting with a letter, at most 40 characters. It must match the file name. |
| `description` | Yes | What the tool does, up to 1,024 characters. Chat and Relays read it to decide when to call the tool. |
| `input` | Yes | A [JSON Schema](https://json-schema.org) with `type: object`. Every call's input is checked against it before the tool runs. |
| `output` | Yes | A JSON Schema with `type: object`. A result that does not match it fails the run. |
| `effect` | No | `read` (default) or `write`. Use `write` when the tool changes something outside Agent Serve, such as sending an email or updating another system. |
| `timeoutMs` | No | Time limit for one run, including starting the sandbox: 1,000 to 300,000. Defaults to 30,000. |
| `packages` | No | npm or PyPI packages the tool imports, pinned the way your lockfile pins them, such as `zod@3.25.76`. At most 50. |
Editors can validate the file against the published schema, `agentserve.tool.schema.json`, with this first line:
```yaml
# yaml-language-server: $schema=https://agentserve.valnoraelric.com/schemas/agentserve.tool.schema.json
```
## The code [#the-code]
A JavaScript or TypeScript tool exports a function named `run` (or a default export). It gets the checked input and returns the output, or a promise of it:
```ts title="tools/score_offer.ts"
export const POLICY = {
maxDiscountPct: { strategic: 25, enterprise: 15, standard: 10 },
minMarginPct: 20,
// …the rest of the policy
} as const
export function run(offer: Offer): OfferScore {
// …checks discount, margin, term and deal size, and collects the reasons
return { score, decision, reasons }
}
```
A Python tool defines a top-level `run(input)`, which may be `async`:
```python title="tools/route_ticket.py"
def run(input):
priority = "high" if input["customer_tier"] == "enterprise" else "normal"
return {"queue": "billing" if "invoice" in input["subject"].lower() else "support", "priority": priority}
```
Keep each tool in its one file. The sandbox receives only the tool's own source, so it cannot import other files from your repository, such as your app's `src/` code. It can import the packages it lists under `packages`. TypeScript is compiled when you upload, so a syntax error, or a module without a `run` export, fails the upload with the file and line.
Make tools pure where you can: no clock, no randomness, and no network unless the tool's job is to call another system. Then the same input always gives the same output, and you can test the function with any test runner without a workspace.
## Deploying tools [#deploying-tools]
`agentserve app deploy` uploads the files your `tools` globs name together with the build output, and a [Git deploy](/developers/git-deploys) always includes them in the bundle. On upload Agent Serve reads every `.tool.yaml`, finds its source, checks both, and stores them with the version. Any problem refuses the whole upload and names the file.
Tool files are stored with the version but never served to the app's frame, so people using the app cannot download your tool's code. An app ships at most 50 tools, and each source file is at most 512 KB.
Tools run from the version a workspace has installed. Publishing and installing a new version, or rolling back, changes the tools for every caller at once.
## Calling a tool [#calling-a-tool]
**From your app**, with the SDK. An app may always run its own tools; there is no separate permission for them.
```ts
import { getAppClient } from '@agentserve/app-sdk'
const run = await getAppClient().runTool('score_offer', offer)
if (run.status === 'succeeded') showScore(run.output)
else showError(run.error)
```
`runTool` resolves with `status` (`succeeded`, `failed` or `timed_out`), `output`, `error` and `durationMs`. A tool that throws, runs out of time or returns something its output schema refuses is a failed run, not an exception. A refusal before the tool starts, such as invalid input or a spent budget, rejects with an [`AppBridgeError`](/developers/sdk#errors). `listTools()` returns the app's tools with their schemas.
**From Chat.** Chat offers each installed app's tools to its agent. When a tool's `effect` is `write`, Chat asks the person before it runs.
**From Relays.** A Relay can call the tools of installed apps. A `write` tool is handled like any other action that changes something outside the workspace: the call waits for approval under the Relay's rules.
**From workflows.** The **App Tool** block runs a tool by its reference, `app-name/tool_name`, such as `deal-desk/score_offer`. The reference is shown on the app's **Manage** page. The block is in preview, so it may not appear in the block list of your workspace yet.
**From the API and the CLI.** `GET /api/v2/apps/tools` lists the tools you may run, and `POST /api/v2/apps/installs/{installId}/tools/{toolName}/run` runs one. Both need your own login or personal API key; a workspace API key is refused, because a tool always runs for a person whose access can be checked.
Whoever calls a tool needs access to the Apps module in the workspace. The person who asked is the one recorded for the run.
## Secrets [#secrets]
A tool that calls another system usually needs a key. Store it as a per-install [secret](/developers/security#secrets) with `agentserve app secrets set STRIPE_KEY` or on the app's **Manage** page. Each secret reaches the tool as an environment variable of the same name:
```ts
const key = process.env.STRIPE_KEY
```
```python
import os
key = os.environ["STRIPE_KEY"]
```
Secrets reach only the tool's own process. They never reach the browser bundle or the app's frame. Any secret value that appears in a tool's error is replaced with `[secret]`, and a run whose output contains a secret value fails with its output withheld.
## Budgets, metering and logs [#budgets-metering-and-logs]
* **Budget.** Tool runs count against the app's monthly [budget](/developers/security#budgets-and-usage), like the workflow runs and Relay tasks it starts. Once the budget is spent, runs are refused with `budget_exhausted` until the next month or until an admin raises it.
* **Metering.** Each run's sandbox time is recorded in the usage ledger as `app-tool` and paid by the workspace of the person who called it, or by the workflow run that called it. The record names the app, the install and the version.
* **Logs.** Every run, whether it succeeded, failed or timed out, is in the workspace audit trail with the tool, the surface that called it, the status and the duration. Inputs, outputs and secrets are never recorded.
The app's **Manage** page has a **Tools** section listing what the installed version ships, the last runs of each tool, and a **Test run** form for builders. A test run is a real run: it is metered, counts against the budget and, for a `write` tool, makes real changes.
## Packages and sandboxes [#packages-and-sandboxes]
Tools run in a remote sandbox, built from the tool's language and its `packages`. Tools that list the same packages share a prepared sandbox image, so they start faster; tools with no packages use the standard Function image. Packages are installed from npm for JavaScript and TypeScript, and from PyPI for Python.
## Limits [#limits]
| | Limit |
| -------------------------- | ------------------------------- |
| Tools per app | 50 |
| Source file | 512 KB |
| `input` or `output` schema | 32 KB each |
| Input of one call | 256 KB |
| Output of one run | 1 MB |
| Time per run | 1 to 300 seconds, 30 by default |
`agentserve app dev` serves your pages from your machine, but tools only run on Agent Serve. In `dev`, `runTool` and `listTools` use the version of your app installed in the workspace, not your local `tools/` files. Deploy, publish and install once to try them; see [agentserve app dev](/developers/cli#agentserve-app-dev).
---
# Deploying from Git (/developers/git-deploys)
You can deploy an app version two ways:
* **From your machine or CI** with `agentserve app deploy`, which builds locally and uploads the result. See [agentserve app deploy](/developers/cli#agentserve-app-deploy).
* **From Git.** Connect a repository, and every push to a branch you choose builds a draft version on Agent Serve.
Both produce the same kind of draft version, which someone then publishes and installs.
## Connect a repository [#connect-a-repository]
Open the app's page under **Apps** → **Library**. In the **Source** section, pick a connection and a repository, then **Connect**. You need write access in the workspace that manages the app, and your permission group must allow building apps.
The connection comes from **Integrations**:
* **GitHub**: an installation of the Agent Serve GitHub App on the account or organization that owns the repository. The section links to the install page if you have none.
* **GitLab**: a personal access token with the `api` scope and the Maintainer role on the project. Connecting creates the project webhook for you.
An app deploys from one repository. To switch, disconnect the current one first. Disconnecting stops new builds and, for GitLab, removes the project webhook when your token allows it. Past deployments and the versions they built stay.
## What a push does [#what-a-push-does]
Agent Serve builds a push when it lands on the repository's **default branch**, or on a branch one of your [branch rules](#branch-rules) names. Pushes to other branches, tags and branch deletions are ignored. The same commit is never built twice for one app.
Each build:
1. Downloads the commit and checks that `agentserve.app.yaml` is at the repository root.
2. Installs dependencies, if there is a `package.json`, with the package manager your lockfile belongs to: Bun (`bun.lock`), npm (`package-lock.json`), pnpm (`pnpm-lock.yaml`) or Yarn (`yarn.lock`). A lockfile is required, and installs never change it.
3. Runs the [dependency audit](#dependency-audit).
4. Runs the build command: `build.command` from the manifest, or `bun run build`.
5. Runs your tests with `test` from `package.json`, if there is one. A failing test fails the build.
6. Packs the bundle and uploads it as a **draft version**, recording the repository, branch and commit it came from.
The bundle is the manifest plus the paths in `build.output`, or by default the folders that hold your page, panel and Chat card entries, the icon and the `requires` files. Your [code tools](/developers/code-tools) are always included. The bundle may be at most 25 MB.
```yaml title="agentserve.app.yaml"
build:
command: npm run build:prod
output: [./web/dist, ./public]
```
Builds run in an isolated sandbox that has no access to workspace data, app secrets or your repository credentials, and stop after 10 minutes. Their compute is metered to the workspace that manages the app.
Agent Serve reports the build back on the commit as a status named **Agent Serve app deploy**, which moves from pending to running to success or failure and links to the deployment. A successful status says whether the version is live or a draft is ready to publish.
## Dependency audit [#dependency-audit]
After install, every build checks your production dependencies:
| Lockfile | Check |
| --------- | ----------------------------------------------------------------------------------------------------------------------------- |
| npm, pnpm | `npm audit` or `pnpm audit` against the public advisory database. Any **high** or **critical** vulnerability fails the build. |
| Bun, Yarn | Every package must come from the public npm or Yarn registry. A package from anywhere else fails the build. |
An audit that cannot run also fails the build, so an outage never lets an unchecked build through. A binary `bun.lockb` cannot be checked: commit the text `bun.lock` instead (`bun install --save-text-lockfile`).
To let a build through anyway, set `build.allowVulnerable: true`. The findings are still recorded on the version and shown wherever it is listed, marked as allowed.
## Branch rules [#branch-rules]
Without rules, every build waits as a draft until someone publishes it.
A **branch rule** auto-publishes builds from one branch and moves chosen workspaces' installs to the new version. For example, `main` can auto-publish and upgrade a test workspace, while production workspaces are upgraded by hand. Rules are set under **Auto-publish** in the **Source** section and need admin access.
* A branch name matches exactly, and a rule's branch is also built even when it is not the default branch.
* Each workspace a rule names must already have the app installed, and you must be an admin there.
* A rule cannot auto-publish while publish approval is on (see below). Its builds wait as drafts instead.
## Publish approval [#publish-approval]
An admin can turn on **Publishing in this workspace** in the **Source** section, so that a second person must approve every publish. The first admin's **Publish** records a request; a different admin's **Publish** approves it and makes the version installable. Nobody can approve their own request. This applies to every publish in the workspace, from the versions list, Deployments, `agentserve app publish` or the API, whether the version was uploaded or built from Git.
## Deployments [#deployments]
The **Deployments** section on the app's page lists every build: commit, author, message, status, the audit result and the build log. A deployment is **Queued**, **Building**, **Ready** (a draft), **Failed** or **Live**.
From there an admin can publish a ready draft (or approve someone else's request), and you can rebuild a failed deployment. To roll back, move an install to an earlier deployment's version.
When a version built from Git goes live, members with write access see a notice in the workspace.
The same information is available through the API: `GET /api/v2/apps/{appId}/repos`, `GET /api/v2/apps/{appId}/deployments` and `GET /api/v2/apps/{appId}/deployments/{deploymentId}`, with `POST …/rebuild` and `POST …/publish` on a deployment. See the [API reference](/api-reference/apps).
## Where a version came from [#where-a-version-came-from]
Every version records its **source**, which the API returns with the version:
| `source.kind` | Fields | Meaning |
| ------------- | ---------------------------------------- | -------------------------------------------------------- |
| `upload` | `fileName` (optional) | Uploaded as an archive, as `agentserve app deploy` does. |
| `git` | `repository`, `commit`, `ref` (optional) | Built from a connected repository at that commit. |
## Setting up the GitHub App [#setting-up-the-github-app]
This is for people who run Agent Serve themselves. On a hosted workspace the GitHub App is already set up; install it from **Integrations**.
Create a GitHub App for your deployment with:
* **Repository permissions**: Contents: read, Metadata: read, Commit statuses: read and write.
* **Events**: subscribe to **Push**.
* **Webhook URL**: `https:///api/apps/git/github`, with a webhook secret.
Then set these on the server: `GITHUB_APP_ID`, `GITHUB_APP_SLUG`, `GITHUB_APP_CLIENT_ID`, `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_PRIVATE_KEY` and `GITHUB_APP_WEBHOOK_SECRET` (the webhook secret). Deliveries whose `X-Hub-Signature-256` does not match the secret are refused.
GitLab needs no server setup. Each connected project gets its own webhook to `/api/apps/git/gitlab` with a secret token of its own, and deliveries without the right token are refused.
---
# Browser IDE (/developers/ide)
The **browser IDE** opens an app's code in VS Code for the web, next to a live preview of the app running against your workspace. You edit, see the change, and commit, without setting anything up on your own machine.
The browser IDE is a **preview**. It is off by default and is turned on per workspace or organization, on top of [Apps](/developers). On a self-hosted deployment, an administrator turns it on and sets up its sandbox. See [Running the IDE on a self-hosted deployment](#running-the-ide-on-a-self-hosted-deployment).
## Open an app in the IDE [#open-an-app-in-the-ide]
On an installed app's **Manage** page, click **Open in IDE**. The button shows only for an app managed from this workspace, and only while the IDE is on.
You need **write** access in the workspace, and your [permission group](/platform/enterprise/access-control#platform) must allow **App Building**. Without both, the IDE page says so and opens nothing.
What the IDE opens depends on the app:
* **An app with a [connected repository](/developers/git-deploys)**: the IDE clones the repository's default branch.
* **An app without one**: the IDE opens the installed version's bundle, read-only. Connect a repository to commit.
The first start takes a moment while the sandbox is created. If you already have a session for the app, the IDE returns to it.
## What the page shows [#what-the-page-shows]
| Panel | What it does |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **VS Code** | VS Code for the web, with the app's code. **New tab** opens it in its own browser tab. |
| **Preview** | The app served by `agentserve app dev`, in the same sandboxed frame people see, using your own access. It reloads as you edit, and restarts by itself after a failed build. |
| **Commit & push** | Commits everything changed in your session with your message and your name, and pushes the branch. |
| **Proposals** | Code changes Chat proposed into this session, each with **Accept** and **Reject**. |
**Stop** ends the session and removes its sandbox.
## Commit and push [#commit-and-push]
**Commit & push** pushes to the connected repository like any other push, so the normal [Git deploy](/developers/git-deploys) runs: a push to the default branch, or to a branch a branch rule names, builds a draft version. You can also use VS Code's own Git panel; it pushes the same way.
Pushing needs a repository connection that allows it. A GitLab connection gives the session a short-lived project token; a GitHub connection uses the Agent Serve GitHub App installation, renewed while the session is active.
## Code changes from Chat [#code-changes-from-chat]
While your session is running, you can ask [Chat](/chat) to change the app's code. Chat proposes the change as a diff into your own session. It never edits files directly:
1. The diff is checked against your working tree and stored as a pending proposal.
2. Under **Proposals**, **Accept** applies it and runs the repository's tests in the sandbox (`test` from `package.json`, or `pytest`). The result shows next to the proposal.
3. **Reject** discards it.
Nothing a proposal does is committed or pushed. That stays your **Commit & push**. A proposal can only reach your own session; Chat cannot write into anyone else's.
## Sessions and cost [#sessions-and-cost]
* Each person has their own session per app. Nobody else can open it.
* A session stops after **30 minutes** without activity, and after **4 hours** in any case.
* The browser reaches the session only through a gate inside the sandbox. Each time you open the IDE or the preview, Agent Serve checks your sign-in and gives the gate a one-time ticket that lasts a minute. A copied link does not work in someone else's browser.
* Sandbox time is metered by the minute to you, in the app's workspace. A session does not start when you, or whoever pays for the workspace, are over a usage limit, and a running session stops when you go over.
* Starting, stopping, committing and deciding a proposal are recorded in the audit trail.
## Running the IDE on a self-hosted deployment [#running-the-ide-on-a-self-hosted-deployment]
The IDE runs each session in a [Daytona](https://www.daytona.io/) sandbox. Other sandbox providers are not supported for the IDE. To turn it on:
1. Turn on Apps (`APPS_RUNTIME=true`) and the IDE (`APP_IDE=true`).
2. Use Daytona as the sandbox provider: `SANDBOX_PROVIDER=daytona` with `DAYTONA_API_KEY` set. See [Sandboxes](/platform/self-hosting/sandboxes).
3. Build the IDE snapshot and point the app at it:
```bash
bun run --cwd packages/agentserve-cli build
DAYTONA_API_KEY=... bun run apps/agentserve/scripts/build-ide-daytona-snapshot.ts --name agentserve-ide:
```
The name needs an explicit tag; Daytona rejects `latest`. Then set `DAYTONA_IDE_SNAPSHOT_ID=agentserve-ide:`.
If something is missing, **Open in IDE** says what: a different sandbox provider, no snapshot set, or a snapshot that does not exist. A snapshot Daytona has deactivated is reactivated, and the IDE asks you to try again in a few minutes.
---
# Build on Agent Serve (/developers)
An **app** is a small web front end that runs inside an Agent Serve workspace. You write the screens your team needs — a deal desk, a field visit log, a support queue — and the workspace provides everything around them.
## What the workspace gives you [#what-the-workspace-gives-you]
You write only your domain code. The rest comes from the workspace the app is installed in:
| You get | How |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Sign-in, people and roles** | People open the app from the workspace, already signed in. The app reads who they are and their role with `useViewer()`. |
| **Data** | Read and add rows in [Tables](/tables), query the semantic model, and read or create workspace [Files](/files). |
| **Models** | Run deployed [workflows](/workflows), hand tasks to [Relays](/relays), or open [Chat](/chat) with a question. Each one uses the models and integrations already set up in the workspace. |
| **Your own logic** | Ship [code tools](/developers/code-tools) with the app: functions that run on Agent Serve, give the same answer every time, and can be called by the app, Chat, Relays and workflows. |
| **Security** | The app acts as the person using it, narrowed to what its manifest names. It runs in a sandboxed frame with no network access of its own. See [Security model](/developers/security). |
| **Metering and audit** | Paid work an app starts, including its code tool runs, is counted against its monthly budget, and every write and tool run is in the audit trail. |
There is no server to run, no login to build, and no database to provision.
## How it fits together [#how-it-fits-together]
1. **Manifest.** `agentserve.app.yaml` names the app, its pages, what it may reach, and what to create on install. See [Manifest reference](/developers/manifest).
2. **Bundle.** You build the front end into static files (HTML, JavaScript, CSS, images). The CLI packs them with the manifest into an archive.
3. **Draft version.** `agentserve app deploy` uploads the archive as a draft version of the app. Or [connect a repository](/developers/git-deploys), and each push builds a draft version on Agent Serve. Where the [browser IDE](/developers/ide) is on, you can also edit and push from the browser.
4. **Publish.** A workspace admin publishes the draft, which makes it installable: `agentserve app publish `.
5. **Install.** An admin installs a published version in a workspace. The install creates the tables, rules and Relays the app declares, if they are missing.
6. **Open.** People open the app from the **Apps** section of the workspace. Admins can pin it to the rail for chosen roles or permission groups.
Apps are turned on per workspace or organization. If you do not see the **Apps** section, Apps is not on for your workspace yet, or your permission group does not include it.
An app can also provide **dashboard panels** and **Chat cards**: small HTML views that render in the same sandboxed frame, inside a dashboard or a Chat reply.
## Where to start [#where-to-start]
---
# Manifest reference (/developers/manifest)
`agentserve.app.yaml` sits at the root of your app. It says what the app is called, which pages it shows, what it may reach, what to create when it is installed, and how much it may spend each month. The platform validates it on every upload, and the install screen lists its permissions for the admin.
## A complete example [#a-complete-example]
```yaml title="agentserve.app.yaml"
# yaml-language-server: $schema=https://agentserve.valnoraelric.com/schemas/agentserve.app.schema.json
name: field-visit-logger
version: 0.1.0
title: Field visit logger
icon: ./dist/icon.svg
pages:
- id: visits
title: Visits
entry: ./dist/index.html#/visits
- id: new-visit
title: New visit
entry: ./dist/index.html#/new
permissions:
tables: [visits]
files: write
relays: [Field follow-up]
requires:
tables: ./tables/*.yaml
relays: ./relays/*.yaml
budget:
monthlyUsd: 25
```
The first line points editors that use the YAML language server (for example VS Code with the YAML extension) at the published JSON Schema, so you get completion and errors as you type. Every template already has it.
## Fields [#fields]
The table below is generated from the manifest's JSON Schema, so it always matches what the platform accepts. A field marked **Required** inside an optional object (such as `budget.monthlyUsd`) is required only when that object is present. A manifest with any field not listed here is refused.
{/* BEGIN GENERATED: app manifest fields (scripts/generate-app-manifest-docs.ts) */}
| Field | Type | Required | Default | Limits | Description |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `string` | Yes | — | matches `^[a-z][a-z0-9-]{1,62}$` | Stable identifier of the app across versions. Choose it once; every version you upload under the same name belongs to the same app. |
| `version` | `string` | Yes | — | a semantic version such as `1.4.0` or `2.0.0-beta.1`; at most 64 characters | Version of this build. Each upload needs a new version; installs move between published versions. |
| `title` | `string` | Yes | — | 1–80 characters | Name people see in the Apps section and on the install screen. |
| `description` | `string` | No | — | 1–160 characters | One line on what the app is for, shown on its card in the Apps hub. |
| `accent` | one of `blue`, `violet`, `teal`, `green`, `amber`, `orange`, `rose` or `slate` | No | — | — | Accent color of the app’s card and tile in the Apps hub. |
| `symbol` | one of `apps`, `chart`, `dashboard`, `table`, `briefcase`, `bank`, `shield`, `users`, `inbox`, `calendar`, `compass`, `sparkles`, `zap`, `rocket`, `workflow` or `document` | No | — | — | Symbol shown on the app’s tile in the Apps hub. |
| `icon` | `string` | No | — | 1–512 characters | Path to an image in the bundle, relative to the manifest. |
| `pages` | `object[]` | Yes | — | 1–20 items | The pages people open from the Apps section, in order. |
| `pages[].id` | `string` | Yes | — | matches `^[a-z][a-z0-9-]{0,62}$` | Page identifier, unique within the app. |
| `pages[].title` | `string` | Yes | — | 1–80 characters | Page name shown in the app’s navigation. |
| `pages[].entry` | `string` | Yes | — | 1–512 characters | HTML file in the bundle, relative to the manifest, optionally with a `#/route` for client-side routing (`./dist/index.html#/home`). |
| `permissions` | `object` | No | see fields | — | What the app may reach. It only narrows: the app always acts as the person using it and never reaches more than they can. |
| `permissions.tables` | `string[]` | No | `[]` | at most 50 items; each 1–255 characters | Tables the app may list, query and add rows to, by table name. |
| `permissions.model` | one of `read` or `none` | No | `"none"` | — | `read` lets the app read and query the semantic model. |
| `permissions.workflows` | `string[]` | No | `[]` | at most 50 items; each 1–255 characters | Deployed workflows the app may run, by workflow name. |
| `permissions.relays` | `string[]` | No | `[]` | at most 50 items; each 1–255 characters | Relays the app may assign tasks to, by Relay name. |
| `permissions.chat` | one of `ask`, `answer` or `none` | No | `"none"` | — | `ask` lets the app open Chat with a question for the person to send; `answer` also lets it get Chat answers back, read-only. |
| `permissions.files` | one of `read`, `write` or `none` | No | `"none"` | — | Workspace files: `read` reads files by id, `write` also creates new files. |
| `permissions.ai` | `object` | No | see fields | — | Platform AI the app may call. Every call counts against the app budget. |
| `permissions.ai.models` | `string[]` | No | `[]` | at most 50 items; each 1–255 characters | Model ids the app may name for `ai.complete` and `ai.media`. Workspace and permission-group model rules still apply. |
| `permissions.ai.media` | `string[]` | No | `[]` | at most 2 items | Media the app may generate with `ai.media`: `image`, `speech`. |
| `permissions.knowledge` | `string[]` | No | `[]` | at most 50 items; each 1–255 characters | Knowledge bases the app may search, by name. |
| `permissions.integrations` | `string[]` | No | `[]` | at most 50 items; each matches `^[a-z][a-z0-9_]{1,127}$` | Integration actions the app may run with the connected accounts of the person using it, by tool id. Actions that change anything ask the person first. |
| `requires` | `object` | No | — | — | Definitions the install creates when the workspace does not already have them. |
| `requires.tables` | `string` or `string[]` | No | — | 1–512 characters; at most 50 items; each 1–512 characters | Glob of table definition files (YAML) in the bundle. |
| `requires.rules` | `string` or `string[]` | No | — | 1–512 characters; at most 50 items; each 1–512 characters | Glob of rule definition files (YAML or JSON) in the bundle. |
| `requires.relays` | `string` or `string[]` | No | — | 1–512 characters; at most 50 items; each 1–512 characters | Glob of Relay definition files (YAML or JSON) in the bundle. |
| `tools` | `string` or `string[]` | No | — | 1–512 characters; at most 50 items; each 1–512 characters | Globs naming your code tools: each `.tool.yaml` and its `.ts`, `.js`, `.mjs` or `.py` source. The files are stored with the version and run on the server; they are never served to the app frame. See Code tools. |
| `chatBlocks` | `string`, `string[]` or `object[]` | No | — | 1–512 characters; at most 50 items; each 1–512 characters | Chat cards the app provides: a glob of HTML files (each named after its file) or a list of cards. |
| `chatBlocks[].id` | `string` | Yes | — | matches `^[a-z][a-z0-9-]{0,62}$` | Card identifier, unique within the app. |
| `chatBlocks[].title` | `string` | Yes | — | 1–80 characters | Card name. |
| `chatBlocks[].entry` | `string` | Yes | — | 1–512 characters | HTML file in the bundle. |
| `panels` | `string`, `string[]` or `object[]` | No | — | 1–512 characters; at most 50 items; each 1–512 characters | Dashboard panels the app provides: a glob of HTML files (each named after its file) or a list of panels. |
| `panels[].id` | `string` | Yes | — | matches `^[a-z][a-z0-9-]{0,62}$` | Panel identifier, unique within the app. |
| `panels[].title` | `string` | Yes | — | 1–80 characters | Panel name. |
| `panels[].entry` | `string` | Yes | — | 1–512 characters | HTML file in the bundle. |
| `build` | `object` | No | — | — | How a Git deploy builds the app. `agentserve app deploy` builds on your machine and ignores it. See Git deploys. |
| `build.command` | `string` | No | — | 1–500 characters | Command run from the repository root after dependencies are installed. Defaults to `bun run build`. |
| `build.output` | `string` or `string[]` | No | — | 1–512 characters; 1–20 items; each 1–512 characters | Files and folders that make up the bundle, besides the manifest. Defaults to the folders holding the page, panel and Chat card entries, the icon and the `requires` files. The `tools` files are always added. |
| `build.allowVulnerable` | `boolean` | No | — | — | Let a build through when the dependency audit finds high or critical vulnerabilities. The version records that it was allowed, and shows it wherever the version is listed. |
| `budget` | `object` | No | — | — | Monthly spending cap for the app’s paid work. An admin can override it per install. |
| `budget.monthlyUsd` | `number` | Yes | — | 0–1,000,000 | Cap in US dollars per calendar month (UTC). |
| `triggers` | `object[]` | No | — | at most 20 items | Platform events and schedules that run a handler in the background, as the admin who approved background runs and only with the scopes they approved. See Plugins. |
| `triggers[].id` | `string` | Yes | — | matches `^[a-z][a-z0-9-]{0,62}$` | Trigger identifier, unique within the app. |
| `triggers[].title` | `string` | Yes | — | 1–60 characters | What the trigger does, shown on the install review and the Manage page. |
| `triggers[].on` | `object` | Yes | — | — | The event: `schedule` (with `cron` and `timezone`), `table.row.inserted` or `table.row.updated` (with `table`), `file.uploaded`, `relay.task.completed`, `workflow.run.completed`, or `calendar.event.ended` (not delivered yet). |
| `triggers[].handler` | `string` | Yes | — | matches `^[a-z][a-z0-9_]{0,39}$` | The code tool that handles the event, by tool name. |
| `commands` | `object[]` | No | — | at most 20 items | Slash commands added to Chat, such as `/call-prep`. |
| `commands[].name` | `string` | Yes | — | matches `^[a-z][a-z0-9-]{1,31}$` | What people type after the slash in Chat. |
| `commands[].description` | `string` | Yes | — | 1–160 characters | Shown next to the command in Chat’s slash menu. |
| `commands[].arguments` | `object[]` | No | `[]` | at most 5 items | Arguments the command asks for, in order. |
| `commands[].arguments[].name` | `string` | Yes | — | matches `^[a-z][a-z0-9_]{0,31}$` | Argument name, as the handler or page receives it. |
| `commands[].arguments[].description` | `string` | Yes | — | 1–160 characters | Shown as the argument’s hint in Chat. |
| `commands[].arguments[].required` | `boolean` | No | `false` | — | Whether the command needs it before it runs. |
| `commands[].run` | `object` | Yes | — | — | `{ handler: }` runs a code tool and shows its answer in Chat; `{ open: { page: } }` opens a page, which reads the arguments with `launch.get`. |
| `actions` | `object[]` | No | — | at most 20 items | Context-menu items on files, table rows, workbook queries and dashboard panels, such as “Summarise this file”. |
| `actions[].id` | `string` | Yes | — | matches `^[a-z][a-z0-9-]{0,62}$` | Action identifier, unique within the app. |
| `actions[].title` | `string` | Yes | — | 1–60 characters | The menu item. |
| `actions[].description` | `string` | No | — | 1–160 characters | A short note shown with the item. |
| `actions[].on` | one of `file`, `table.row`, `workbook.query` or `dashboard.panel` | Yes | — | — | Where it appears: `file`, `table.row`, `workbook.query` or `dashboard.panel`. |
| `actions[].types` | `string[]` | No | — | at most 20 items; each an extension such as `.pdf` or a media type such as `image/*` | For `file`: only files with these extensions (`.pdf`) or media types (`image/*`). |
| `actions[].tables` | `string[]` | No | — | at most 50 items; each 1–255 characters | For `table.row`: only rows of these tables, each also named in `permissions.tables`. Defaults to every table there. |
| `actions[].run` | `object` | Yes | — | — | `{ handler: }` runs a code tool with the file, row, query or panel; `{ open: { page: } }` opens a page that reads it with `launch.get`. |
| `settings` | `object` | No | — | — | A JSON Schema form an admin fills in on the Manage page. Pages read it with `settings.get`; code tools get it in their input. |
| `settings.type` | `string` | No | `"object"` | — | Always `object`. |
| `settings.properties` | `object` | Yes | — | — | The fields, by key: `string` (with `enum`, `format: textarea`, `email`, `uri` or `secret`), `number`, `integer` or `boolean`, each with a `title`. A `secret` field’s key is a secret name, such as `API_TOKEN`; its value is kept in the install’s secrets. |
| `settings.required` | `string[]` | No | `[]` | at most 30 items | Keys of the fields an admin must fill in. |
| `connectors` | `object[]` | No | — | at most 10 items | MCP servers the plugin brings: created on install through the workspace’s MCP stack, owned by the install and removed when it is uninstalled. The plugin calls them with `mcp.call`. |
| `connectors[].id` | `string` | Yes | — | matches `^[a-z][a-z0-9-]{0,62}$` | How the plugin names the connector in `mcp.call`. |
| `connectors[].title` | `string` | Yes | — | 1–60 characters | Shown on the install review and the Manage page. |
| `connectors[].directory` | `string` | No | — | matches `^[a-z][a-z0-9-]{0,62}$` | A connectors directory entry, such as `linear`, which supplies the address and sign-in. |
| `connectors[].url` | `string` | No | — | at most 2048 characters | The remote MCP endpoint (https), when no directory entry is named. |
| `connectors[].auth` | one of `none`, `oauth` or `header` | No | — | — | `oauth`: an admin connects an account on the Manage page. `header`: a secret setting is sent as a header. `none`: no sign-in. |
| `connectors[].header` | `object` | No | — | — | For a key sent as a header: which secret setting, under which header. |
| `connectors[].header.name` | `string` | Yes | — | matches `^[A-Za-z][A-Za-z0-9-]{0,63}$` | The header, such as `Authorization`. |
| `connectors[].header.secret` | `string` | Yes | — | matches `^[A-Z][A-Z0-9_]{0,63}$` | The secret setting whose value is sent. |
| `connectors[].header.prefix` | `string` | No | — | at most 32 characters | Sent before the value, such as `Bearer `. |
| `connectors[].tools` | `string[]` | No | — | at most 100 items; each 1–128 characters | Only these tools may be called. Omitted means every tool the server lists. |
| `skills` | `object[]` | No | — | at most 20 items | Instruction packs added to the workspace skill library, usable as `/name` in Chat and by Relays. |
| `skills[].name` | `string` | Yes | — | matches `^[a-z0-9]+(-[a-z0-9]+)*$`; at most 64 characters | Skill name, as Chat types it after a slash. |
| `skills[].description` | `string` | Yes | — | 1–1024 characters | What the skill is for, shown in the skill library. |
| `skills[].file` | `string` | Yes | — | matches `\.md$`; 1–512 characters | The Markdown instructions in the bundle. |
| `background` | `object` | No | — | — | Limits for background runs: how many at once and how long each may take. |
| `background.concurrency` | `integer` | No | `1` | 1–5 | Most background runs of one install at once. |
| `background.timeoutSeconds` | `integer` | No | `120` | 10–480 | Longest one run may take before it is stopped. |
| `background.monthlyUsd` | `number` | No | — | 0–100,000 | Suggested monthly cap on background spend, shown on the install review. The admin sets the real cap. |
| `listing` | `object` | No | — | — | What the plugin catalog shows about a first-party plugin. |
| `listing.tagline` | `string` | No | — | 1–120 characters | One short line under the plugin’s name in Discover. |
| `listing.categories` | `string[]` | No | `[]` | at most 3 items | Catalog categories, at most three. |
| `listing.packs` | `string[]` | No | `[]` | at most 5 items | Team packs the plugin belongs to: `everyone`, `sales`, `finance`, `legal`, `ops`. |
| `listing.screenshots` | `string[]` | No | `[]` | at most 8 items; each a `.png`, `.jpg` or `.webp` file; each 1–512 characters | Screenshots in the bundle, shown on the plugin’s detail page. |
| `listing.publisher` | `object` | No | — | — | Who publishes the plugin. Defaults to Agent Serve for first-party plugins. |
| `listing.publisher.name` | `string` | Yes | — | 1–80 characters | Publisher name. |
| `listing.publisher.url` | `string` | No | — | at most 512 characters | Publisher web address. |
| `slots` | `object` | No | — | — | Workspace resources the admin picks at install, by slot name: each has a `kind` (`table`, `knowledge_base`, `workflow`, `relay` or `model`), a `title`, a `description`, `required` (default true) and an optional `default` name. The app addresses each by its slot name. |
{/* END GENERATED: app manifest fields */}
## Paths and globs [#paths-and-globs]
Every path in the manifest is relative to the manifest, such as `./dist/index.html` or `./tables/*.yaml`. A path may not start with `/`, may not be a URL, and may not leave the bundle with `..`.
Globs match files in the bundle: `*` matches within one folder, `**` matches any number of folders, and `?` matches one character. Fields that take a glob also take a list of globs.
When you upload, the platform checks that:
* every page entry, panel entry and Chat card entry is an HTML file in the bundle,
* the icon, when set, is in the bundle,
* `requires.tables`, when set, matches at least one file.
## Pages [#pages]
Each page is one entry in the Apps section's navigation. Most apps are a single-page application with one `index.html` and a route per page, written as a fragment: `./dist/index.html#/visits`. The app reads the fragment to decide what to show. You can have up to 20 pages, and their `id`s must be unique.
## Permissions [#permissions]
`permissions` names what the app may reach. The app always acts as the person using it, so a permission never gives anyone more access than they already have. It only narrows. See [Security model](/developers/security).
| Permission | What it opens |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tables: [name, …]` | List those tables and their columns, query their rows, and add rows. |
| `model: read` | Read the semantic model's entities, attributes and measures, and run governed queries on it. |
| `workflows: [name, …]` | Run those deployed workflows, waiting for the result or queued. |
| `relays: [name, …]` | Assign tasks to those Relays. |
| `chat: ask` | Open Chat with a question. The person decides whether to send it. |
| `chat: answer` | Everything `ask` opens, and get Chat's answer back in the app. Chat answers read-only: it cannot change anything from there. |
| `files: read` | Read workspace files by id. |
| `files: write` | Read workspace files and create new ones. |
| `ai: { models: [id, …] }` | Ask those chat models for completions, whole or streamed. |
| `ai: { media: [image, speech] }` | Generate images or speech with the workspace's media models, or with a model also listed in `ai.models`. |
| `knowledge: [name, …]` | Search those knowledge bases. |
| `integrations: [action, …]` | Run those integration actions, such as `slack_send_message`, with the person's own connected account. An action that changes anything asks the person first. |
```yaml
permissions:
ai:
models: [gpt-5.5]
media: [image]
knowledge: [Product docs]
integrations: [slack_send_message]
chat: answer
```
Model, media and Chat calls run on the workspace's own provider key when it has one, and on Agent Serve's hosted key otherwise. Either way the person's permission group decides which providers and models they may use, so an app cannot reach a model the person could not. The app never sees a credential: integration actions use the person's connected account on the server, and the frame only gets the result.
Names are matched against the workspace the app is installed in, when each call is made:
* A name with no match grants nothing. Calls naming it fail with `not_found`.
* If several workflows share a name, the app cannot run any of them until one is renamed.
* A call that names a resource the manifest does not list fails with `forbidden`, even if the person could reach it.
## Slots [#slots]
A name under `permissions` must match a resource in every workspace the app is installed in. When workspaces call the same thing by different names — a pipeline table, a docs knowledge base — declare a **slot** instead. The admin picks the workspace's own resource for each slot on the install review, and can change it later on the Manage page. The app calls methods with the slot's name.
```yaml
slots:
pipeline:
kind: table
title: Pipeline table
description: Accounts and their open deals come from this table.
default: pipeline_opportunities
docs:
kind: knowledge_base
title: Knowledge base
description: Questions are answered from this one.
required: false
```
| Field | What it means |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `kind` | What the admin picks: `table`, `knowledge_base`, `workflow`, `relay` or `model`. |
| `title`, `description` | What the review and the Manage page show next to the picker. Say why the app needs it. |
| `required` | Defaults to `true`. The app installs only once a required slot is bound, or its `default` exists. |
| `default` | A name the slot uses when the admin binds nothing and the workspace has a resource called that. The review starts on it. |
* A slot is a permission by itself. A `table` slot lets the app reach the one table the admin bound to it, exactly as listing that table under `permissions.tables` would, and every call is still held to the person's own access.
* Call methods with the slot name: `tables.query({ table: 'pipeline' })`, `knowledge.search({ knowledgeBases: ['docs'] })`, `workflows.run('intake')`, `relays.assignTask({ relay: 'reviewers' })`.
* A `model` slot names a chat model the admin picks; pass the slot name as `model` in `ai.complete`. The workspace's semantic model is not a slot: there is one per workspace, reached with `permissions.model: read`.
* A slot whose resource is deleted or archived falls back to its default, else it is unbound. Calls to an unbound slot fail with `not_found` and say an admin picks it on the Manage page, where it shows as **Needs setup**.
* `tables.list` marks a table bound to a slot with that slot's name, and `viewer.get` lists each slot and whether it is set up.
* A slot's name must not also appear under `permissions` for the same kind.
* Triggers, actions and background scopes may name a slot wherever they name a table, workflow, Relay or knowledge base.
Literal names keep working: a name under `permissions` is a fixed slot whose only binding is that name.
## Requires: tables, rules and Relays [#requires-tables-rules-and-relays]
`requires` lists definitions the install creates when the workspace does not already have one with the same name. Existing ones are left as they are. The install is safe to retry: anything it already created is skipped.
### Tables [#tables]
One YAML file per table:
```yaml title="tables/visits.yaml"
name: visits
description: Customer visits logged in the field
columns:
- { name: customer, type: string, required: true }
- { name: visited_on, type: date, required: true }
- { name: outcome, type: string }
- { name: deal_value, type: currency }
- { name: follow_up, type: boolean }
```
| Field | Rules |
| -------------------- | ------------------------------------------------------------------------------------------- |
| `name` | Required, 1–255 characters. List the same name under `permissions.tables` to use the table. |
| `description` | Optional, up to 2,000 characters. |
| `columns` | 1–200 columns, each with a unique `name` (1–255 characters, compared without case). |
| `columns[].type` | One of `string`, `number`, `boolean`, `date`, `json`, `currency`. |
| `columns[].required` | Optional `true` or `false`. |
| `columns[].unique` | Optional `true` or `false`. |
A table that cannot be created fails the whole install, with the file and field in the error. The installing admin's own plan limits apply.
### Rules [#rules]
One YAML or JSON file per rule. The file is a rule definition exactly as the Rules API takes it, with a non-empty `name`; the rule service validates it in full when the install creates it.
Rules need model management. If the installing admin's permission group does not include it, or a definition is invalid, the install still succeeds and reports the rule as **skipped** with the reason, so someone can create it in Rules.
### Relays [#relays]
One YAML or JSON file per Relay. The install creates the Relay, sets it up from the file, and publishes it when the definition is complete and the admin leaves **Publish now** on:
```yaml title="relays/legal-review.yaml"
# yaml-language-server: $schema=https://agentserve.valnoraelric.com/schemas/agentserve.relay.schema.json
name: Legal review
description: Reviews contracts flagged by Contract triage
specialization: legal
instructions: |
You review contracts that Contract triage flagged against the team's playbook.
For every flagged clause, accept, counter with exact wording, or escalate.
delivers:
summary: A decision for every flagged clause, with the reason and any counter wording.
as: task_result
escalation: Escalate anything that touches liability caps, indemnities or personal data.
autonomy: assist
approvals: { approvers: installer, expireAfterHours: 48 }
tools: [workspace_read, tables, knowledge]
workflows:
- workflow: Send redlines
responsibilities:
- name: Weekly review backlog
objective: List contracts that have waited more than five working days.
schedule: { cron: '0 9 * * 1', timezone: UTC }
```
| Field | Rules |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | Required, 1–120 characters. List the same name under `permissions.relays` to use it. |
| `description` | Optional, up to 2,000 characters. |
| `specialization` | Optional. Defaults to `custom`. |
| `instructions` | What the Relay does and how, in Markdown. Together with `delivers`, at most 8,000 characters. |
| `delivers` | What it hands back when a task is done: a sentence, or `{ summary, as, folder }` where `as` is `task_result` (default) or `library_file`, which also saves a file in `folder`. It is added to the instructions under "What you deliver". |
| `escalation` | Optional. When it stops and asks a person. |
| `language`, `exampleTasks` | Optional. Its working language and up to 10 example tasks. |
| `autonomy` | `observe` (default: drafts only), `assist` (acts within its rules, asks for the rest) or `operate`. |
| `approvals` | Optional. `approvers: installer` (default) makes the installing admin its approver; `none` leaves approvers for later. `expireAfterHours` is 1–168. |
| `tools` | Optional tool groups: `workspace_read`, `knowledge`, `tables`, `web`, `integrations`, `workflows`. |
| `integrations.services` | Optional integration services it may call, by id, such as `slack`. |
| `workflows` | Optional deployed workflows it may run, by name, with `autonomous` (default `false`). A name that matches no workflow, or several, is left for the admin to choose. |
| `limits` | Optional `maxConcurrentTasks` (1–10) and `maxTasksPerDay` (1–200). |
| `responsibilities` | Optional scheduled routines (`name`, `objective`, `schedule.cron`, `schedule.timezone`). They are created turned off. |
The editor schema is published as `agentserve.relay.schema.json`.
A definition is **complete** when it has `instructions` and `delivers`. The install review shows each Relay with its instructions and a **Publish now** switch, on by default for a complete definition when the admin may publish Relays. Everything happens as the installing admin, through the same steps as the Relays page: the Relay is created, its setup saved, its routines added and, when chosen, published, with the same setup and go-live checks. When a step cannot finish (a workflow the workspace does not have, a check that fails, a permission group without Relays), the Relay stays a draft and the review lists exactly what is missing, with a link to finish its setup.
Relays are created only when the app is installed from the **Apps** page in the workspace. An install from the CLI or the API reports each declared Relay as skipped, with the reason, so create it in Relays or install from the workspace instead.
## Panels and Chat cards [#panels-and-chat-cards]
An app can provide small views that render outside its own pages, in the same sandboxed frame:
* `panels` — dashboard panels people add to a [dashboard](/dashboards).
* `chatBlocks` — cards Chat can show in a reply.
Declare them as a list with an `id`, `title` and `entry` each, or as a glob of HTML files:
```yaml
panels:
- id: pipeline
title: Pipeline
entry: ./dist/panels/pipeline.html
chatBlocks: ./dist/cards/*.html
```
With a glob, each matching HTML file becomes one component whose `id` and `title` come from its file name, lowercased, with other characters turned into `-`. A file whose name does not make a valid id is left out, and at most 50 are used. A panel or card can call `resize` from the SDK to fit its height to its content.
## Tools [#tools]
`tools` names your code tools, such as `./tools/*`: each `.tool.yaml` and its `.ts`, `.mts`, `.js`, `.mjs` or `.py` source. They are uploaded with the version, checked on upload, stored with it, and run on Agent Serve for the app, Chat, Relays and workflows. They are never served to the app's frame. See [Code tools](/developers/code-tools).
## Plugin sections [#plugin-sections]
An app becomes a plugin when it also adds things to the workspace's own surfaces. Each section is optional:
* `triggers` run a handler on a schedule or when something happens in the workspace, in the background.
* `commands` add slash commands to Chat.
* `actions` add menu items to files, table rows, workbook queries and dashboard panels.
* `settings` is a form an admin fills in for the install.
* `connectors` bring MCP servers (tools from other services) the plugin calls.
* `skills` add instruction packs to the workspace's skill library.
* `background` limits background runs; `listing` is what the plugin catalog shows.
A handler is one of the app's own [code tools](/developers/code-tools), named by its tool name; an upload is refused when a handler names a tool the version does not ship. Every resource a trigger or action names must also be in `permissions`. See [Plugins](/developers/plugins) for each section, the handler input and output, and how background runs are authorized.
## Build [#build]
`build` tells a [Git deploy](/developers/git-deploys) how to build the app: the command to run and which paths make up the bundle. `agentserve app deploy` builds on your machine and ignores it.
## Budget [#budget]
`budget.monthlyUsd` caps what the app's paid work may spend in a calendar month, measured in UTC. Paid work is the workflow runs, Relay tasks and code tool runs the app starts, its model completions, generated media, Chat answers and knowledge searches, and the hosted-key cost of its integration actions. Once the month's spend reaches the cap, those calls fail with `budget_exhausted` until the next month.
An admin can set a different budget for an install on its **Manage** page, which takes precedence over the manifest. With no budget in either place, the app's spend is not capped. See [Budgets](/developers/security#budgets-and-usage).
---
# Plugins (/developers/plugins)
A **plugin** is an app that does more than show pages. Besides its own pages, it can add things to the places your team already works:
* **Commands** in Chat, such as `/call-prep` or `/brief`.
* **Actions** in menus: "Summarise this file" on a file, "Explain this query" on a workbook query, and so on.
* **Triggers** that run on a schedule or when something happens: a row is added, a file is uploaded, a Relay finishes a task, a workflow run ends.
* **Settings** an admin fills in once per install.
* **Connectors**: tools from other services, through MCP (the Model Context Protocol, an open standard for connecting AI agents to outside tools).
* **Skills**: instructions Chat and Relays follow, added to the workspace skill library.
There is nothing separate to learn. A plugin is an app with more sections in its [manifest](/developers/manifest): the same bundle, the same `agentserve app deploy`, the same install review and the same security model.
## A worked example [#a-worked-example]
The first-party **File briefs** plugin (`plugins/file-briefs/` in the repository) uses most of the sections:
```yaml title="agentserve.app.yaml"
# yaml-language-server: $schema=https://agentserve.valnoraelric.com/schemas/agentserve.app.schema.json
name: file-briefs
version: 1.0.0
title: File briefs
description: Short, plain briefs of your files, on demand from any file and for every new file in Inbox.
accent: violet
symbol: document
pages:
- id: briefs
title: Briefs
entry: ./web/index.html
permissions:
tables: [File briefs]
files: read
ai:
models: [claude-haiku-4-5]
requires:
tables: ./tables/*.yaml
tools: ./tools/file_brief.*
triggers:
- id: inbox-upload
title: Log every file uploaded to Inbox, ready for a brief
on:
event: file.uploaded
folder: Inbox
types: [.md, .txt, .csv, .json]
handler: file_brief
commands:
- name: brief
description: Open your briefs, or find one by file name
arguments:
- name: file
description: Part of a file name to look for
run:
open:
page: briefs
actions:
- id: summarise-file
title: Summarise this file
on: file
types: [.md, .txt, .csv, .json]
run:
handler: file_brief
settings:
type: object
properties:
tone:
type: string
title: Brief style
enum: [Plain, Bullet points, Executive summary]
default: Plain
maxLines:
type: integer
title: Longest brief
minimum: 3
maximum: 12
default: 5
required: [tone]
skills:
- name: brief-writing
description: Write a short, plain brief of any document for a busy reader.
file: ./skills/brief-writing.md
background:
concurrency: 2
timeoutSeconds: 60
monthlyUsd: 5
listing:
tagline: A brief of any file in five lines.
categories: [productivity]
packs: [everyone, legal, ops]
budget:
monthlyUsd: 20
```
Every field is listed in the [manifest reference](/developers/manifest#fields).
## Handlers [#handlers]
A **handler** is one of the app's own [code tools](/developers/code-tools), named by its tool name in a trigger, command or action (`handler: file_brief`). The upload is refused if a handler names a tool the version does not ship.
### What a handler receives [#what-a-handler-receives]
Every handler gets the same input, so one tool can handle several entries:
```ts
interface PluginHandlerInput {
kind: 'trigger' | 'command' | 'action'
install: { id: string; workspaceId: string; version: string }
/** Non-secret settings. Secret settings are environment variables of the same name. */
settings: Record
trigger?: { id: string } // kind: trigger
event?: { type: string; [key: string]: unknown }
command?: { name: string; args: Record } // kind: command
action?: { id: string; target: ActionTarget } // kind: action
viewer?: { userId: string } // absent in the background
}
```
For an action, the target comes with the content the plugin may read already attached, read as the person who chose it:
| Action on | `target` |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `file` | `{ kind, fileId, name, contentType, text? }`. `text` is the file's text, up to 200,000 characters, when the manifest has `files: read` and the file is text. |
| `table.row` | `{ kind, table, rowId, row }`. `row` holds the row's values by column name. |
| `workbook.query` | `{ kind, workbookId, cellId, title, sql }` |
| `dashboard.panel` | `{ kind, dashboardId, panelId, title }` |
The tool's `.tool.yaml` input schema must accept this shape. The simplest is an object with `kind`, `install` and `settings` required and the rest optional, as `plugins/file-briefs/tools/file_brief.tool.yaml` does.
### What a handler returns [#what-a-handler-returns]
A handler never calls the network or Agent Serve itself. It returns what to do, and Agent Serve does it:
```ts
interface PluginHandlerResult {
/** Shown to the person: in Chat for a command, in a notice for an action. */
message?: string
/** Open one of the plugin's pages; it reads what it was opened for with launch.get. */
open?: { page: string; params?: Record }
/** Up to 20 SDK calls, made in order after the handler finishes. */
calls?: Array<{ method: string; params: object }>
/** The index of a call whose answer becomes the message. */
answerWith?: number
}
```
`calls` are the same calls an app page makes, such as `tables.insertRows`, `ai.complete`, `relays.assignTask` or `mcp.call`. `answerWith` turns the text of an `ai.complete`, `chat.answer` or `mcp.call` into the message, so a handler can build a prompt and let Agent Serve run it. File briefs answers "Summarise this file" this way:
```ts title="tools/file_brief.ts"
export function run(input: PluginHandlerInput): PluginHandlerResult {
const target = input.action?.target
if (!target?.text?.trim()) return { message: 'This file has no text to brief.' }
return {
calls: [
{
method: 'ai.complete',
params: {
model: 'claude-haiku-4-5',
messages: [
{ role: 'system', content: `Write a brief in at most ${input.settings.maxLines ?? 5} lines.` },
{ role: 'user', content: target.text },
],
},
},
],
answerWith: 0,
}
}
```
The SDK exports these types and a `definePluginHandler` helper; see [App SDK](/developers/sdk#plugins).
### How calls are authorized [#how-calls-are-authorized]
Every call a handler hands back goes through the same checks as a call from an app page: the person it acts for, narrowed to what the manifest names, with the app's budget and per-install limits. When a person ran the command or action, the calls act as them. A call that would need their approval (an integration action or MCP tool that changes something) is skipped and logged; open the plugin's page to make it with their approval. A handler cannot run another code tool through its calls.
Calls run after the handler finishes, so a handler cannot read a call's result. For work in several steps, hand it to a Relay with `relays.assignTask`.
## Commands [#commands]
A command adds `/name` to Chat's slash menu, with its description and arguments. When someone chooses it, Chat asks for the arguments, then either:
* **runs the handler** (`run: { handler: … }`) and shows its message in Chat, or
* **opens a page** (`run: { open: { page: … } }`). The page reads the command and its arguments with `launch.get`.
## Actions [#actions]
An action adds a menu item where the manifest says (`on`):
| `on` | Where it appears |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `file` | The menu on a file in Files, and the file viewer's menu. `types` limits it to extensions (`.pdf`) or media types (`image/*`). |
| `table.row` | The menu on a table row. `tables` limits it to some tables; each must be in `permissions.tables`. |
| `workbook.query` | A query cell's toolbar in a workbook. |
| `dashboard.panel` | A dashboard panel's menu. |
Like a command, an action runs its handler and shows the message, or opens a page that reads the target with `launch.get`.
A plugin page that needs a file the action did not bring asks for one with `pickFiles`: Agent Serve shows its own file picker, and the plugin receives only what the person chose. A page can also put text on the clipboard with `copyText` and save a file with `download`; the app frame has no clipboard or downloads of its own, so Agent Serve does both from the host page. See [Files](/developers/sdk#files) and [Clipboard and downloads](/developers/sdk#clipboard-and-downloads) in the SDK reference.
## Triggers [#triggers]
A trigger runs its handler in the background when its event happens. The event's details arrive as the handler's `event`:
| `event` | Fires when | Filters | `event` carries |
| ------------------------ | ------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `schedule` | The cron expression comes round | `cron` (five fields), `timezone` (IANA, default `UTC`) | `scheduledFor`, `cron`, `timezone` |
| `table.row.inserted` | A row is added to the table | `table`, which must be in `permissions.tables` | `table`, `row` (values by column name) |
| `table.row.updated` | A row of the table changes | `table`; `columns` fires only when one of them changes | `table`, `row`, `previous`, `changedColumns` |
| `file.uploaded` | A file is uploaded to workspace files | `folder` (a path such as `Contracts/2026`, including its subfolders), `types` | `file` (`id`, `name`, `contentType`, `size`, `folderId`, `folderPath`) |
| `relay.task.completed` | A Relay completes a task | `relay`, which must be in `permissions.relays` | `relay`, `task` (`id`, `title`, `state`) |
| `workflow.run.completed` | A workflow run ends | `workflow` (in `permissions.workflows`), `status` (`success`, `error`) | `workflow`, `executionId`, `status`, `durationMs` |
| `calendar.event.ended` | A calendar event ends | `calendar` | — |
`calendar.event.ended` is defined so plugins can declare it, but it is **not delivered yet**: the connected calendars report changes, not when an event ends. A trigger on it installs, shows on the Manage page as waiting for its event source, and never runs.
Rows written by a bulk import do not fire table triggers. Each delivery runs a trigger once, even if it is retried.
## Background runs [#background-runs]
A trigger runs with nobody in the app. Its runs act as **the admin who approved background runs** for the install, and only with **the scopes that admin approved**: a subset of the manifest's permissions and connectors, ticked on the install review or the Manage page. Every run is authorized again, when it starts, as that admin: their current workspace role, their permission group, and the Apps module. If they lose access, background runs stop and the Manage page says why; another admin approves them again to take over. A scope the manifest does not ask for is never granted, even if an approval names it.
Because the admin approved the scopes in advance, a background run's calls do not wait for anyone's approval.
| Limit | What it does |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `background.concurrency` | Most runs of one install at once (1 to 5, default 1). Others wait their turn, for up to an hour. |
| `background.timeoutSeconds` | Longest one run may take (10 to 480 seconds, default 120). |
| Monthly background cap | Set by the admin. When background spend reaches it, background runs pause until next month or until an admin raises it. `background.monthlyUsd` suggests one. |
| App budget | Background runs count against the app's monthly budget like any other call. |
| Rate | At most 60 runs per install per minute from events, so a plugin cannot loop on its own activity. |
| Kill switch | An admin pauses and resumes background runs on the Manage page. |
Every run — background or a person's command or action — appears in the install's **run history** on the Manage page: when it started and ended, its outcome (succeeded, failed, timed out, or refused before it started), what it cost and a short log of what Agent Serve did. The log never holds the handler's input, its output values or secrets.
## Settings [#settings]
`settings` is a small JSON Schema form an admin fills in on the Manage page (and can fill in on the install review). Fields are `string` (optionally an `enum`, or `format: textarea`, `email` or `uri`), `number`, `integer` or `boolean`, each with a `title`, an optional `description` and `default`. `required` lists the fields that must have a value.
* Pages read the values with `settings.get` / `useSettings()`.
* Handlers get them in their input as `settings`.
* A field with `format: secret` is kept in the install's secrets, never sent to a page and never shown again after it is saved. Its key is the secret's name (`API_TOKEN`), and code tools read it as an environment variable of that name.
## Connectors [#connectors]
`connectors` names MCP servers the plugin brings: by a [connectors directory](#connectors-directory) entry (`directory: linear`) or by URL (`url: https://…`, with `auth`).
* On install, each connector becomes an MCP server in the workspace, through the workspace's own MCP settings: the admin's `mcp_tools.use` permission and the workspace's allowed MCP domains apply. A server the workspace already had at the same address is used, not replaced.
* The install review shows each connector's host, how it signs in and the tools it allows (`tools`, or every tool it lists).
* An `oauth` connector is signed in from the Manage page. A `header` connector sends a secret setting as a header (`header: { name: Authorization, secret: LINEAR_TOKEN, prefix: 'Bearer ' }`).
* The plugin calls a connector with `mcp.call` (`callMcp` in the SDK): only its own connectors, only the tools the manifest allows, as the person using it or, in the background, as the approving admin with the connectors they approved.
* A connector the plugin created is its own: Chat and Relays do not see it unless an admin shares it with them on the Manage page. Uninstalling the plugin removes it.
## Skills [#skills]
`skills` adds instruction packs, one Markdown file each, to the workspace's skill library. People use them in Chat as `/name`, and Relays can use them like any other workspace skill. A name the workspace already uses is skipped and listed on the install review. Uninstalling the plugin removes the skills it added.
## Slots [#slots]
A plugin that reads a workspace's own data — its pipeline table, its docs — declares [slots](/developers/manifest#slots) instead of naming them. The install review asks for each one ("Needs a knowledge base. Questions are answered from this one."), with a searchable list of the workspace's own tables, knowledge bases, workflows or Relays, and a link to create one when there is none. Handlers and their calls use the slot names too, and background runs reach a slot only when the admin approved it among the background scopes.
## Installing and reviewing [#installing-and-reviewing]
Admins install a plugin the same way as any app. The review lists, in plain words:
* the resource each slot needs, with a picker;
* what the plugin may read and change, and the tables, rules and Relays it creates, with each Relay's instructions and a **Publish now** switch (see [Relays](/developers/manifest#relays));
* the commands, actions and triggers it adds;
* its connectors (host, sign-in, tools) and skills;
* its settings;
* and, when it has triggers, the background scopes to approve and the monthly cap.
Background runs can also be approved later from the Manage page.
## The plugin catalog [#the-plugin-catalog]
The Apps hub's **Discover** view lists the first-party plugins this deployment ships, by category and by team pack (Everyone, Sales, Finance, Legal, Ops), with a detail page for each: what it does, screenshots, the permissions in plain words, what it adds, the publisher and its version history. An admin installs one with one click and the same review.
First-party plugins live in the repository under `plugins//`, in the same layout as an app (`agentserve.app.yaml`, sources, `tools/`, a `README.md` and an optional `CHANGELOG.md` with `## 1.2.0 (2026-10-01)` headings). To publish one:
1. Add or change `plugins//` (the manifest `name` must be the folder name).
2. Run `bun run generate:plugin-catalog`. It validates every manifest and writes the catalog Discover reads; `--check` fails when the catalog is stale.
3. The image build runs `bun run build:plugin-bundles`, which builds each plugin and packs exactly what `agentserve app deploy` would upload, with its checksum. Installing reads that bundle and checks the checksum.
The first install in an organization creates the plugin's app; later installs, and newer catalog versions, use the same app. A workspace app that already has the plugin's name is never replaced.
## Connectors directory [#connectors-directory]
Discover also lists popular remote MCP servers that any workspace can connect with one click, without a plugin. Each entry shows its publisher, its tools, how it signs in, the scopes it asks for and a risk level worked out from what its tools do:
| Risk | Means |
| --------------- | ------------------------------------------------------------- |
| Read only | Its tools only read. |
| Can change data | Some tools create, update or delete in the service. |
| Can act outside | Some tools send messages, act on live websites or move money. |
Connecting shows that review first. A server that signs in with OAuth opens the vendor's sign-in; one that needs an API key or a registered OAuth app asks for it. Every entry's address and sign-in were checked against the vendor's own documentation.
## Developing a plugin [#developing-a-plugin]
`agentserve app dev` runs a plugin's pages like any app's. For plugins it also:
* prints what the plugin adds;
* answers `settings.get` with each field's default, overridden by `.agentserve/settings.json` in the app folder;
* answers `launch.get` with `--launch '{"kind":"command","name":"brief","args":{"file":"q3"}}'` when you pass it;
* lists files with `listFiles` through the workspace's files API as you, and shows a simple picker for `pickFiles`;
* copies and downloads from the dev page, with the same rules for unsafe file types;
* refuses `mcp.call`, which runs only in the workspace.
Handlers are code tools, so they run on the server from an installed version. Deploy, publish and install the plugin, then try its commands, actions and triggers in the workspace; the Manage page's run history shows each run.
---
# Quickstart (/developers/quickstart)
This page takes the blank template from an empty folder to an app people can open in your workspace.
## Before you start [#before-you-start]
* Install the [Agent Serve CLI](/cli) and sign in with `agentserve login`.
* Have [Bun](https://bun.sh) or Node.js 20 or later for the app's build.
* To publish and install, you need the **admin** role in the workspace. Uploading a draft needs **write**. See [Roles](/developers/security#roles).
### Create the app [#create-the-app]
```bash
agentserve app init my-app
cd my-app
bun install # or: npm install
```
`init` copies the `blank` template into `my-app`, sets the manifest `name` to `my-app` (from the folder name) and its `title` to `My app`, and copies the app SDK into `vendor/app-sdk`. It refuses a folder that is not empty unless you pass `--force`.
To start from another template, pass `--template`. `agentserve app init --list` prints them all. See [Templates](/developers/templates).
The folder now holds:
```text
my-app/
├── agentserve.app.yaml # the manifest
├── index.html
├── package.json
├── src/ # your React code
├── tables/items.yaml # a table the install creates
├── vendor/app-sdk/ # the SDK, copied by init
└── vite.config.ts # builds into dist/
```
### Run it locally [#run-it-locally]
```bash
agentserve app dev
```
`dev` builds the app and serves it at `http://localhost:5175`, inside a local host page that frames it with the same sandbox and content security policy as the workspace. SDK calls go to your workspace through the API with your own CLI login, so the app sees exactly what you can see, still narrowed to what the manifest names.
When you change a file, `dev` rebuilds and reloads the frame. It ignores `dist/`, `node_modules/` and `vendor/`.
The blank app reads a table called `items`. Until you install the app, that table only exists if you create it yourself, so the items card shows an error. Everything else works.
A few calls behave differently in `dev`; see [agentserve app dev](/developers/cli#agentserve-app-dev).
### Deploy a draft [#deploy-a-draft]
```bash
agentserve app deploy
```
`deploy` runs the build, packs the manifest, the folders your pages live in (here `dist/`), the icon, the files under `requires` and any [code tools](/developers/code-tools) into a `.zip`, and uploads it as a **draft version**. It prints the version, `0.1.0`, and a link to the app's versions page in the workspace.
A draft cannot be installed yet. Each upload needs a new `version` in the manifest; uploading a version that already exists fails.
### Publish the version [#publish-the-version]
```bash
agentserve app publish 0.1.0
```
Publishing makes the draft installable. It needs the admin role.
### Install it [#install-it]
```bash
agentserve app install
```
`install` installs the highest published version in your current workspace (pass `-w ` for another one). It prints what it created — here the `items` table — and anything it skipped.
### Open it [#open-it]
In the workspace, open **Apps** in the left rail and pick **My app** (the title `init` made from the folder name; pass `--title` to choose another). On the app's **Manage** page an admin can pin it to the rail for chosen roles or groups, set a monthly budget, and see spend and activity.
## Ship a change [#ship-a-change]
1. Edit the code and raise `version` in `agentserve.app.yaml`, for example to `0.2.0`.
2. Deploy and publish, moving your workspace's install to the new version in one step:
```bash
agentserve app deploy
agentserve app publish 0.2.0 --rollout
```
3. If something is wrong, move back: `agentserve app rollback 0.1.0`.
## Next steps [#next-steps]
* Add permissions and pages: [Manifest reference](/developers/manifest).
* Call workflows, Relays and Chat from your code: [App SDK](/developers/sdk).
* Understand what admins approve: [Security model](/developers/security).
---
# App SDK (/developers/sdk)
`@agentserve/app-sdk` is how an app talks to its workspace. Every call goes from the app's sandboxed frame to the page around it, which checks the call and runs it as the person using the app, narrowed to the app's [manifest](/developers/manifest). The app has no other way out: it cannot make network requests of its own.
## Install [#install]
`agentserve app init` copies the SDK into your app at `vendor/app-sdk`, and the template's `package.json` depends on it there:
```json title="package.json"
"dependencies": {
"@agentserve/app-sdk": "file:./vendor/app-sdk"
}
```
Run `bun install` (or `npm install`) once after `init`. The SDK has three entry points:
| Import | What it holds |
| ------------------------------ | ------------------------------------------------------------------ |
| `@agentserve/app-sdk` | `AppClient`, `getAppClient`, `AppBridgeError` and the result types |
| `@agentserve/app-sdk/react` | React hooks and one-call functions (React 19) |
| `@agentserve/app-sdk/protocol` | The bridge protocol, its limits and schemas |
## React [#react]
Read hooks load when the component mounts and again when their inputs change. Each returns the same shape:
```ts
interface AppQueryState {
data: T | undefined // the latest answer; undefined until it arrives, or after an error
error: AppBridgeError | Error | undefined
loading: boolean // true while a load is in flight, including a refetch
refetch: () => void // load again with the same inputs, for example after a write
}
```
An answer that arrives after a newer request, or after the component unmounts, is ignored.
```tsx
import { useTable, writeRows } from '@agentserve/app-sdk/react'
export function Items() {
const items = useTable('items', 50)
async function add(title: string) {
await writeRows('items', [{ title, done: false }])
items.refetch()
}
if (items.loading) return
Loading…
if (items.error) return
{items.error.message}
return (
{items.data?.rows.map((row) => (
{String(row.data.title)}
))}
)
}
```
### Hooks [#hooks]
| Hook | Returns | Needs |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `useViewer()` | The person using the app, their role, and the app's identity. See [Viewer](#viewer). | Nothing |
| `useTables()` | `{ tables }`: the tables the manifest names that the person can read, with their columns. | `permissions.tables` |
| `useQuery(params)` | `{ rows, nextCursor }` for a [table query](#tables). Reloads when `params` changes by value. | The table in `permissions.tables` |
| `useTable(table, limit = 200)` | The first `limit` rows of a table. | The table in `permissions.tables` |
| `useModel()` | The semantic model's entities, attributes and measures. | `model: read` |
| `useModelQuery(params)` | Rows of a [model query](#semantic-model). Reloads when `params` changes by value. | `model: read` |
| `useKnowledgeSearch(params)` | `{ results }` of a [knowledge search](#knowledge). Reloads when `params` changes by value; pass `null` to wait. | The knowledge bases in `permissions.knowledge` |
| `useCompletion()` | `{ run, cancel, text, data, error, loading }` for a [streamed completion](#ai-models): `text` grows as the model writes, and `cancel()` stops it. | The model in `permissions.ai.models` |
| `useFiles(params)` | `{ folders, files, nextCursor }`: a page of the [workspace files](#files) the person can see. Reloads when `params` changes by value. | `files: read` or `write` |
| `useSettings()` | `{ values, secretsSet }`: the install's [settings](#plugins). | Nothing |
| `useLaunch()` | What the page was opened for by a Chat command or an action, or `null`. See [Plugins](#plugins). | Nothing |
### Functions [#functions]
These run once and return a promise. Use them for writes, and for reads you trigger yourself.
| Function | Resolves to | Needs |
| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------ |
| `writeRows(table, rows)` | `{ rows }`: the rows added | The table in `permissions.tables` |
| `runWorkflow(workflow, input?)` | The run's result, after it finishes | The workflow in `permissions.workflows` |
| `queueWorkflow(workflow, input?)` | `{ executionId, status: 'queued' }`, once queued | The workflow in `permissions.workflows` |
| `assignRelayTask({ relay, title, objective, acceptance? })` | `{ taskId, relayId }` | The Relay in `permissions.relays` |
| `askChat(prompt)` | `{ opened: true }` | `chat: ask` |
| `readFile(fileId)` | The file and its content | `files: read` or `write` |
| `writeFile({ name, content, encoding?, contentType? })` | `{ id, name, contentType, size }` | `files: write` |
| `readModel()` | The semantic model | `model: read` |
| `queryModel(params)` | Rows of a model query | `model: read` |
| `complete(params)` | `{ model, content, tokens, costUsd }` | The model in `permissions.ai.models` |
| `streamComplete(params, onDelta)` | The same, after handing each piece of the answer to `onDelta` | The model in `permissions.ai.models` |
| `generateMedia({ kind, prompt, … })` | `{ kind, model, mimeType, encoding: 'base64', content, costUsd }` | The kind in `permissions.ai.media` |
| `answerChat(prompt)` | `{ content, costUsd }` | `chat: answer` |
| `searchKnowledge(params)` | `{ results }` | The knowledge bases in `permissions.knowledge` |
| `runIntegration({ action, input?, account? })` | `{ status, output, error }` | The action in `permissions.integrations` |
| `callMcp({ connector, tool, arguments? })` | `{ status, text, content, error }` | The connector in `connectors` |
| `listFiles(params?)` | `{ folders, files, nextCursor }` | `files: read` or `write` |
| `pickFiles({ multiple?, types?, title? })` | `{ files }`: what the person chose in Agent Serve's file picker | `files: read` or `write` |
| `copyText(text)` | `{ copied: true }`, once the text is on the person's clipboard | Nothing |
| `download({ filename, content, encoding?, mimeType? })` or `download({ fileId, filename? })` | `{ started: true }`, once the browser saves the file | Nothing; `files: read` or `write` for a `fileId` |
`complete`, `streamComplete` and `answerChat` take a last `{ signal }` argument: aborting the `AbortSignal` cancels the call (see [Cancelling](#cancelling)).
## AppClient [#appclient]
The React functions use one shared client. Outside React, or for anything the hooks do not cover, use it directly:
```ts
import { AppBridgeError, getAppClient } from '@agentserve/app-sdk'
const client = getAppClient()
const viewer = await client.getViewer()
```
`getAppClient()` creates the page's client on first use. `new AppClient({ timeoutMs })` creates another; it only works inside an app frame.
| Method | Bridge method |
| --------------------------------- | -------------------------------------------------- |
| `getViewer()` | `viewer.get` |
| `listTables()` | `tables.list` |
| `query(params)` | `tables.query` |
| `writeRows(table, rows)` | `tables.insertRows` |
| `runWorkflow(workflow, input?)` | `workflows.run` |
| `queueWorkflow(workflow, input?)` | `workflows.run` with `async: true` |
| `assignRelayTask(params)` | `relays.assignTask` |
| `readFile(fileId)` | `files.read` |
| `writeFile(params)` | `files.write` |
| `readModel()` | `model.read` |
| `queryModel(params)` | `model.query` |
| `listTools()` | `tools.list` |
| `runTool(name, input?)` | `tools.run` |
| `complete(params)` | `ai.complete` |
| `streamComplete(params, onDelta)` | `ai.complete` with `stream: true` |
| `generateMedia(params)` | `ai.media` |
| `answerChat(prompt)` | `chat.answer` |
| `searchKnowledge(params)` | `knowledge.search` |
| `runIntegration(params)` | `integrations.run` |
| `getSettings()` | `settings.get` |
| `callMcp(params)` | `mcp.call` |
| `getLaunch()` | `launch.get` |
| `listFiles(params?)` | `files.list` |
| `pickFiles(params?)` | `files.pick` |
| `copyText(text)` | `ui.copy` |
| `download(params)` | `ui.download` |
| `askChat(prompt)` | `chat.ask` |
| `resize(height)` | `ui.resize` |
| `onTheme(listener)` | Theme events; returns an unsubscribe function |
| `call(method, params)` | Any bridge method, typed by method name |
| `close()` | Stops listening and fails every call still waiting |
A call that gets no answer within 60 seconds fails with `timeout`; model, media, Chat answer and integration calls wait up to 300 seconds. Pass `timeoutMs` to a new client to change it.
## Methods in detail [#methods-in-detail]
### Viewer [#viewer]
`getViewer()` / `useViewer()` returns:
| Field | Type |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `userId`, `name`, `email` | The person using the app. `name` and `email` may be `null`. |
| `role` | Their workspace role: `read`, `write` or `admin`. |
| `workspaceId` | The workspace the app is installed in. |
| `app` | `{ id, installId, versionId, version, slug }` of the running install. |
| `permissions` | The installed version's `permissions`, with defaults filled in. Slot names appear in the list of their kind. |
| `slots` | Each slot the manifest declares, by name: `{ kind, title, bound, model }`. `bound` says whether it is set up; `model` is the bound model id for a `model` slot, and `null` otherwise. A bound table's or knowledge base's own name is not shown, since the person may not be able to see it. |
Call methods with a slot's name; the server reaches the resource the admin bound. `tables.list` returns each table's `slot` (its slot name, or `null`), so a page can find the table a slot is bound to. Under `agentserve app dev`, slots resolve from `.agentserve/slots.json` in the app folder — `{ "pipeline": "Deals" }`, slot name to the workspace resource's name — else from each slot's `default`.
### Tables [#tables]
`query(params)` / `useQuery(params)` takes:
| Param | Rules |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `table` | A table name listed in `permissions.tables`. |
| `where` | Optional. Column name to value; every condition must hold. A value is a string (up to 10,000 characters), number, boolean, or `null` for an empty cell. At most 20 conditions. |
| `sort` | Optional. Up to 5 of `{ column, direction }`, where `direction` is `asc` or `desc`. |
| `columns` | Optional. Up to 200 column names to return. |
| `limit` | Optional. 1–500 rows, default 100. |
| `cursor` | Optional. The `nextCursor` of the previous page. |
It returns `{ rows, nextCursor }`. Each row is `{ id, data }`, with `data` keyed by column name. `nextCursor` is `null` on the last page.
`writeRows(table, rows)` adds 1–100 rows per call, each keyed by column name with at most 200 columns. It returns the added rows. The person must be allowed to add rows to the table.
Every row, column and row-security rule that applies to the person applies to the app too.
### Workflows [#workflows]
`runWorkflow(name, input)` runs a deployed workflow and waits for it. It resolves to `{ executionId, status, output, error }`, where `status` is `completed`, `failed`, `paused` or `cancelled`. A run that starts but fails still resolves, with `status: 'failed'` and the `error` message; the call itself rejects only when the run cannot start.
`queueWorkflow(name, input)` queues the run and resolves with `status: 'queued'` and the execution id, without output. Both count against the app's [budget](/developers/security#budgets-and-usage).
### Relays [#relays]
```ts
await assignRelayTask({
relay: 'Field follow-up',
title: 'Follow up with Acme',
objective: 'Send Acme the pricing sheet and book a call next week.',
acceptance: 'A call is booked.',
})
```
`title` is 1–200 characters, `objective` 1–8,000, `acceptance` up to 4,000. The task is assigned as the person using the app, and its cost counts against the app's budget.
### Chat [#chat]
`askChat(prompt)` opens Chat in the workspace with the prompt filled in. The person reads it and decides whether to send it. The prompt is 1–4,000 characters, and an app can open Chat at most once every 5 seconds.
`answerChat(prompt)` asks Chat and returns its answer to the app, without opening Chat; it needs `chat: answer`. Chat answers as the person, from a read-only turn: it can look things up across the workspace, but any tool that would change something is refused. The conversation is saved to the person's Chat list under the app's name, so they can see what was asked for them. The turn is billed like any Chat turn and also counts against the app's budget.
```ts
const { content } = await answerChat('Which accounts renewed late this quarter, and why?')
```
### AI models [#ai-models]
`complete(params)` sends a conversation to a chat model the manifest lists in `ai.models` and resolves with the answer:
```ts
const { content, costUsd } = await complete({
model: 'gpt-5.5',
messages: [
{ role: 'system', content: 'You write short, friendly product descriptions.' },
{ role: 'user', content: `Describe: ${product.name}` },
],
maxTokens: 300,
})
```
| Param | Rules |
| ---------------- | -------------------------------------------------------------------------------------------------------- |
| `model` | A model id listed in `permissions.ai.models`. |
| `messages` | 1–50 of `{ role, content }`, where `role` is `system`, `user` or `assistant`; 200,000 characters in all. |
| `maxTokens` | Optional. 1–8,192. |
| `temperature` | Optional. 0–2. |
| `responseFormat` | Optional. `json` asks for a single JSON object; parse `content` yourself. Defaults to `text`. |
`streamComplete(params, onDelta)` and `useCompletion()` deliver the answer in pieces as the model writes it, then resolve with the whole answer and its cost:
```tsx
import { useCompletion } from '@agentserve/app-sdk/react'
function Summary({ notes }: { notes: string }) {
const { run, text, loading } = useCompletion()
return (
{text}
)
}
```
The person's permission group still decides which providers and models they may use, and a model the deployment blocks is refused with `forbidden`. A call runs on the workspace's own provider key when it has one, and on Agent Serve's hosted key otherwise; `costUsd` is what it was billed, and it counts against the app's budget.
`generateMedia({ kind, prompt, model?, aspectRatio?, voice? })` makes an image or a clip of speech; `kind` must be listed in `ai.media`. Without `model` the workspace's default model for the kind is used; a `model` must also be listed in `ai.models`. The result is base64 content with its MIME type, up to 8 MB:
```ts
const image = await generateMedia({ kind: 'image', prompt: 'A lighthouse at dawn', aspectRatio: '16:9' })
setSrc(`data:${image.mimeType};base64,${image.content}`)
```
### Knowledge [#knowledge]
`searchKnowledge({ knowledgeBases, query, topK? })` / `useKnowledgeSearch(params)` searches 1–5 knowledge bases the manifest names, by name, and returns up to `topK` passages (1–20, default 5), best match first. Each result is `{ knowledgeBase, documentId, documentName, sourceUrl, content, similarity }`. Only documents the person can read are searched.
```ts
const { results } = await searchKnowledge({ knowledgeBases: ['Product docs'], query: 'refund policy' })
```
### Integrations [#integrations]
`runIntegration({ action, input, account })` runs an integration action the manifest lists in `integrations`, such as `slack_send_message`, with the person's own connected account. The app never sees the account's credentials. When the person has several accounts for the provider, pass `account` with the one's name; otherwise it is chosen for them.
An action that changes anything (sending, creating, updating, deleting, paying) is first put to the person in the workspace page. If they approve, it runs; if not, the call fails with `declined`. Reads run straight away.
```ts
const run = await runIntegration({
action: 'slack_send_message',
input: { channel: '#sales', text: `New lead: ${lead.name}` },
})
if (run.status === 'failed') showNotice(run.error)
```
It resolves with `{ status, output, error }`: `status` is `failed` when the provider refused the action. The action's input is the integration's own, the same parameters the tool takes in a workflow.
Under `agentserve app dev`, `searchKnowledge` runs against the workspace with your own access, while model, media, Chat answer and integration calls fail with `not_found` because they are metered to an installed app: give your code a fallback, or deploy and open the app in the workspace.
### Files [#files]
`readFile(fileId)` returns `{ id, name, contentType, size, encoding, content }`. Text files (text, JSON, XML, CSV, YAML, Markdown) come back as `utf8`; anything else as `base64`. Files up to 5 MB can be read. A file that may contain secret values is refused with `forbidden`.
`writeFile({ name, content, encoding, contentType })` creates a new workspace file. `encoding` is `utf8` (the default) or `base64`, for binary content such as a photo. A file is at most 5 MB. If the name is taken, the new file gets a numbered suffix.
`listFiles({ folder?, search?, types?, recursive?, limit?, cursor? })` / `useFiles(params)` lists the workspace files and folders the person can see, so an app can build its own file browser. It needs `files: read` or `write`, and returns exactly what the person could see in Files: their file and folder access applies.
| Param | Rules |
| ----------- | ------------------------------------------------------------------------------------------------------ |
| `folder` | Optional. A folder path such as `Contracts/2026`; the workspace root when left out. |
| `search` | Optional. Words in the file name, 1–200 characters. |
| `types` | Optional. Up to 20 extensions (`.pdf`) or media types (`application/pdf`, `image/*`). |
| `recursive` | Optional. Include files in subfolders. Defaults to `true` with a `search`, `false` without. |
| `limit` | Optional. 1–100 files, default 50. |
| `cursor` | Optional. The `nextCursor` of the previous page; it works only with the same folder, search and types. |
`folders` (the folder's subfolders, or folders whose name matches the search) comes with the first page only. Files come in name order as `{ id, name, contentType, size, folderPath, updatedAt }`. Types are matched after a page is read, so a page filtered by type can hold fewer than `limit` files while `nextCursor` still leads on.
`pickFiles({ multiple?, types?, title? })` is usually the better choice: Agent Serve shows its own file picker over the app, the person chooses, and the app receives only the files they chose (`{ id, name, contentType, size, folderPath, updatedAt }`). Files outside `types` are shown but cannot be chosen. Closing the picker rejects with `declined`. It needs `files: read` or `write`; read a chosen file's content with `readFile`.
```ts
const { files } = await pickFiles({ types: ['.pdf', '.docx'], title: 'Choose a contract' })
const contract = await readFile(files[0].id)
```
### Clipboard and downloads [#clipboard-and-downloads]
An app runs in a sandboxed frame with no clipboard and no downloads of its own. Agent Serve does both for it, from the host page.
`copyText(text)` puts up to 100,000 characters on the person's clipboard and shows a short "Copied" notice. Call it from a click: browsers copy only right after the person does something.
`download(params)` saves a file to the person's computer and shows a "Saved" notice:
* `{ filename, content, encoding?, mimeType? }` saves content the app made. `encoding` is `utf8` (the default) or `base64`. The file is at most 10 MB.
* `{ fileId, filename? }` saves a workspace file the person can read; it needs `files: read` or `write` and is at most 5 MB, the `readFile` limit.
Text, CSV, JSON, XML, Markdown, PDF, images, audio and video, Office documents (`.xlsx`, `.docx`, `.pptx`) and zip archives are saved as they are. Pages and scripts — HTML, SVG, JavaScript and executables — are saved as plain text with `.txt` added, so a downloaded file never runs when opened. Any other type is refused with `invalid_request`. The name loses any folder path and characters a file system refuses.
Why not let the frame copy directly? The frame has an opaque origin and runs whatever code the app ships, so granting it `clipboard-write` would let any script in it write the clipboard at any moment, with no way to tie it to a person's click on Agent Serve's side. Copying through the host page keeps every write capped, visible ("Copied") and on Agent Serve's terms, and the same holds for downloads.
### Cancelling [#cancelling]
`complete`, `streamComplete` and `answerChat` accept `{ signal }`, and `useCompletion()` returns `cancel()`. Aborting stops the call and rejects it with `cancelled`.
```ts
const controller = new AbortController()
stopButton.onclick = () => controller.abort()
await streamComplete(params, (delta) => append(delta), { signal: controller.signal })
```
A cancelled streamed completion stops at the model: Agent Serve aborts the provider request, and the app is billed only for what the model wrote. The provider reports tokens only when a stream finishes, so a cancelled one is metered from the text sent and the text written (counted the same way Agent Serve counts tokens elsewhere), priced like any other completion, and recorded as cancelled. A completion that has not streamed yet (`complete`), and `answerChat`, stop waiting when cancelled; the work already started in the workspace finishes and is billed as usual.
### Semantic model [#semantic-model]
`readModel()` / `useModel()` returns `{ source, entities }`. `source` is `published`, or `draft` when the person is working in their draft model. Each entity lists its `attributes` (with a `role`) and `measures` by name.
`queryModel(params)` / `useModelQuery(params)` runs a governed query:
```ts
const result = await queryModel({
measures: [{ entity: 'Orders', measure: 'Revenue' }],
dimensions: [{ entity: 'Orders', attribute: 'Order date', dateGrain: 'month' }],
where: { 'Orders.Account': 'Acme' },
limit: 12,
})
```
| Param | Rules |
| ------------ | --------------------------------------------------------------------------------------------------------------------- |
| `measures` | 1–16 of `{ entity, measure }`. An entity is named by id or by name. |
| `dimensions` | Optional. Up to 4 of `{ entity, attribute, dateGrain? }`; `dateGrain` is `day`, `week`, `month`, `quarter` or `year`. |
| `where` | Optional. Up to 20 equality conditions keyed `entity.attribute`. |
| `sort` | Optional. Up to 5 of `{ by, direction }`, where `direction` is `asc` or `desc`. |
| `limit` | Optional. 1–500 rows, default 100. |
It returns `{ columns, rows, truncated, warnings }`. The query reaches only rows the person could read in the tables behind the model.
### Code tools [#code-tools]
`runTool(name, input?)` runs one of the app's own [code tools](/developers/code-tools) on the server and resolves with `{ status, output, error, durationMs }`. `status` is `succeeded`, `failed` (the tool threw or returned something its output schema refuses) or `timed_out`; only a refusal before the run, such as invalid input or a spent budget, rejects. The call waits up to 330 seconds, longer than the longest tool limit. `listTools()` returns the app's tools with their input and output schemas. An app needs no permission to run its own tools, and each run counts against its budget.
```ts
const run = await getAppClient().runTool('score_offer', offer)
if (run.status === 'succeeded') setScore(run.output)
```
### Plugins [#plugins]
A plugin is an app that also adds things to the workspace: Chat commands, menu items, background triggers, settings, connectors and skills. See [Plugins](/developers/plugins). Three methods are for plugins:
* `getSettings()` / `useSettings()` returns `{ values, secretsSet }`: the values an admin saved on the install's Manage page, with each field's default where nothing was saved. A secret setting is never sent to a page; `secretsSet` names the secret settings that have a value, and only the plugin's code tools receive them.
* `getLaunch()` / `useLaunch()` returns what the page was opened for: `{ kind: 'command', name, args }` when a Chat command opened it, `{ kind: 'action', id, target }` when a menu item did, or `null` when someone opened it from the Apps hub. The page answers this itself, so it costs nothing.
* `callMcp({ connector, tool, arguments })` calls a tool on one of the plugin's own connectors (the manifest `connectors`), as the person using it. A tool that changes anything is put to the person first, like an integration action, and fails with `declined` if they say no. It resolves with `{ status, text, content, error }`: `text` joins the text the tool returned.
```ts
const { values } = await getAppClient().getSettings()
const launch = await getAppClient().getLaunch()
if (launch?.kind === 'command') setQuery(launch.args.file ?? '')
```
Plugin handlers are code tools, so they do not use the client. The SDK exports their types instead: `PluginHandlerInput`, `PluginHandlerResult` and `definePluginHandler`, which types a handler and returns it unchanged:
```ts title="tools/file_brief.ts"
import { definePluginHandler } from '@agentserve/app-sdk'
export const run = definePluginHandler((input) => {
if (input.kind !== 'action' || !input.action?.target.text) return { message: 'Nothing to brief.' }
return {
calls: [{ method: 'ai.complete', params: { model: 'claude-haiku-4-5', messages: [{ role: 'user', content: input.action.target.text }] } }],
answerWith: 0,
}
})
```
A tool's source runs on its own, so to use the types in a handler, import them with `import type` or copy them in.
Under `agentserve app dev`, `settings.get` answers with each field's default, overridden by `.agentserve/settings.json` in the app folder; `launch.get` answers with `--launch ''` when you pass it; and `mcp.call` fails with `not_found`, because connectors run only in the workspace.
## Theme and size [#theme-and-size]
The workspace sends the app a theme event, `light` or `dark`, when it mounts the frame and whenever the person switches theme. The first event can arrive before your code subscribes, so start from a default and follow the events:
```tsx
import { useEffect, useState } from 'react'
import { getAppClient } from '@agentserve/app-sdk'
export function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
useEffect(() => getAppClient().onTheme(setTheme), [])
return theme
}
```
Panels and Chat cards can fit their height to their content with `resize(height)`, a whole number of pixels from 0 to 20,000:
```ts
getAppClient().resize(document.body.scrollHeight)
```
## Errors [#errors]
A failed call rejects with an `AppBridgeError`. Its `code` is stable and safe to branch on; its `message` is for people.
```ts
import { AppBridgeError } from '@agentserve/app-sdk'
try {
await runWorkflow('Route ticket', { ticketId })
} catch (error) {
if (error instanceof AppBridgeError && error.code === 'budget_exhausted') {
showNotice('This app has used its budget for the month.')
} else throw error
}
```
| Code | When |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request` | The parameters break the protocol or a limit on this page, or the workspace refused them as invalid (for example a file over 5 MB, or a `where` key not written `entity.attribute`). |
| `unknown_method` | The method is not part of the protocol. |
| `forbidden` | The manifest does not name the table, workflow, Relay, knowledge base, model or integration action, or does not grant the permission (including `chat: ask`); the person lacks access, or their permission group withholds the model; or a file may contain secret values. |
| `not_found` | Nothing in the workspace has that name or id: a table, workflow, Relay, file, semantic entity, code tool, knowledge base or model; or the person has no connected account for an integration action. |
| `conflict` | The app is turned off in this workspace, several workflows or knowledge bases share the name, a workflow run could not start, or the person has several accounts for an integration and the call did not name one. |
| `rate_limited` | More than 20 calls in one second from the app, Chat opened again within 5 seconds, a per-minute ceiling on model, media, Chat answer, knowledge or integration calls, or a workspace rate limit. |
| `budget_exhausted` | The app has spent its monthly budget. Workflow runs, Relay tasks, code tool runs, and model, media, Chat answer, knowledge and integration calls check it. |
| `declined` | The person declined an integration action that would have changed something, or closed the file picker. |
| `cancelled` | The app cancelled the call with its `AbortSignal`. |
| `timeout` | No answer within the client's timeout (60 seconds by default). |
| `internal` | Something failed in the workspace, or the client was closed. |
## Limits [#limits]
| Limit | Value |
| ------------------------------------------- | ------------------------------------------------------------------- |
| Rows per query | 1–500, default 100 |
| Rows per `writeRows` call | 100 |
| Columns per row written | 200 |
| `where` conditions | 20 |
| Sort keys | 5 |
| Chat prompt | 4,000 characters |
| File read or write | 5 MB |
| Files per `listFiles` page | 1–100, default 50 |
| Copied text | 100,000 characters |
| Download | 10 MB of content; 5 MB for a workspace file |
| Model query measures | 16 |
| Model query dimensions | 4 |
| Relay task objective | 8,000 characters |
| Model call messages | 50, 200,000 characters in all |
| Model call output | 8,192 tokens |
| Media prompt | 4,000 characters; result up to 8 MB |
| Knowledge search | 1–5 knowledge bases, 1–20 results |
| Integration action input | 64 KB |
| MCP tool call arguments | 64 KB; counted with integration actions per minute |
| Calls one plugin handler hands back | 20 |
| Model calls per minute, per install | 30 |
| Media generations per minute, per install | 10 |
| Chat answers per minute, per install | 6 |
| Knowledge searches per minute, per install | 60 |
| Integration actions per minute, per install | 20 |
| Calls per second | 20 |
| Call timeout | 60 seconds; 300 for model, media, Chat answer and integration calls |
The constants behind these limits are exported from `@agentserve/app-sdk/protocol`, for example `APP_QUERY_MAX_LIMIT` and `APP_INSERT_MAX_ROWS`.
---
# Security model (/developers/security)
An app holds no authority of its own. It acts **as the person using it**, narrowed to what its manifest names, from a sandboxed frame that cannot reach the network. This page explains each layer.
## The app acts as the person using it [#the-app-acts-as-the-person-using-it]
Every SDK call is made as an **app principal**: the signed-in person, the app, and the installed version, together. The workspace authorizes each call again as that person, at the moment of the call:
* their current workspace role and permission group,
* their access to each table, workflow, Relay and file,
* row security and hidden columns on every table.
It then narrows the result to the manifest. Effective access is what the person can reach **and** what the manifest names, never more. Naming a table in the manifest gives the app nothing the person could not already read, and a person with access to a table the manifest does not name cannot reach it through the app.
Each manifest permission opens a fixed set of operations, defined in Agent Serve's code. A manifest names resources and access levels, never operations, so it cannot ask for anything outside this list:
| Permission | Operations |
| -------------- | -------------------------------------------------------------------- |
| `tables` | List tables, read table definitions, query rows, add rows |
| `workflows` | Run a workflow |
| `relays` | Assign a Relay task |
| `files: read` | Read file details and content |
| `files: write` | The above, plus create files |
| `model: read` | Read the semantic model, and query rows through it |
| `chat: ask` | Open Chat with a prompt; the person decides whether to send it |
| `chat: answer` | The above, plus get Chat's answer to a prompt, from a read-only turn |
| `ai.models` | Call those chat models |
| `ai.media` | Generate those media kinds |
| `knowledge` | Search those knowledge bases |
| `integrations` | Run those integration actions with the person's connected account |
A semantic model query reaches the tables behind the model for that one call. Each of those tables is still authorized as the person, so the model reaches no row they could not read.
Model and media calls are held to the person's permission group (allowed providers, denied models, the media switch) and the deployment's blocked providers, as well as to `ai.models`. A Chat answer runs as a read-only turn: Chat is told the person may only read, is offered no integration tools, and any tool that would change something is refused rather than run.
An integration action runs on the server with the person's own connected account; credentials never reach the frame. When the person has several accounts for the provider, the app names one with `account`. An action that changes anything (sending, creating, updating, deleting, paying) stops for the person's approval in the workspace page first, the same way Chat asks before it changes something. The app cannot skip that step: only the page around the frame can mark a call approved, and it does so only after the person clicks **Run it**. If they decline, the call fails with `declined`. Each approved action is recorded in the audit log.
## The sandboxed frame [#the-sandboxed-frame]
An app's pages, panels and Chat cards run in an iframe with `sandbox="allow-scripts"`. It always has an opaque origin, wherever it is shown:
* **No cookies or browser storage**, and no same-origin access to Agent Serve.
* **No network.** The content security policy sets `connect-src 'none'`, so `fetch`, `XMLHttpRequest` and WebSockets are refused. The only way out is the SDK, through messages to the page around the frame.
* **Only its own files.** Scripts, styles, images, fonts and media load only from the app's own bundle. Inline scripts are refused, so ship scripts as files; inline styles are allowed. Forms, nested frames, workers and plugins are refused.
* **No device access.** Camera, microphone, location, payment and USB are turned off.
The page around the frame answers messages only from that frame's own window, checks every message against the SDK protocol, and limits each frame to 20 calls a second. Calls it does not answer itself go to the workspace, which checks them again.
Bundle files are served under a **signed, short-lived frame token** instead of a cookie. The token binds the person, the workspace, the install and the version that were authorized when the app was opened, and it expires after one hour.
A bundle may contain only static files: HTML, JavaScript, CSS, JSON, source maps, text, Markdown, YAML, CSV, images (SVG, PNG, JPEG, GIF, WebP, AVIF, ICO), fonts (WOFF, WOFF2, TTF, OTF) and media (MP3, MP4, WebM). An upload with any other file type is refused. See [archive limits](/developers/cli#agentserve-app-deploy).
## What an admin approves [#what-an-admin-approves]
An app reaches a workspace only after an admin acts twice:
1. **Publish** a draft version, which makes it installable.
2. **Install** a published version in the workspace.
The install screen lists the version's permissions: each table, workflow and Relay by name, and whether it may read the model, open Chat, or read or write files. Installing also creates the tables, rules and Relays the app declares under `requires` that the workspace does not already have, using the installing admin's own access and plan limits.
Changing the installed version — upgrading, or rolling back — is an admin action too, and a new version can ask for different permissions. An admin can also turn an install off, which makes every SDK call fail with `conflict`, or uninstall it.
A version that any workspace still runs cannot be retired. An app is managed from the workspace that first uploaded it.
## Roles [#roles]
| Action | Minimum role |
| -------------------------------------------------------------- | ------------ |
| Open and use installed apps | `read` |
| List apps, read app details, upload a draft version | `write` |
| Publish or retire a version | `admin` |
| Install, uninstall, change the version, pins, budget or status | `admin` |
| Read an install's spend and audit trail | `admin` |
| List, set or delete secrets | `admin` |
[Permission groups](/platform/enterprise/access-control) can narrow this further: a group can hide the Apps module, which blocks using and building apps, or turn off building and managing apps on its own. Opening an app and making SDK calls work only from a signed-in browser session, never from an API key.
## Secrets [#secrets]
Admins can store per-install secrets with `agentserve app secrets set` or on the **Manage** page.
* Secrets are **write-only**. A value is encrypted when it is stored, and nothing returns it: listing shows names and when each was last changed.
* Names use capital letters, digits and underscores and start with a letter, up to 64 characters (`STRIPE_KEY`). A value is at most 16 KB.
* Setting and deleting a secret is recorded in the audit trail.
Secrets never reach the browser bundle or the app's frame, and the SDK has no method to read them. They reach only the app's [code tools](/developers/code-tools#secrets), as environment variables of the tool's own process on the server. A secret value in a tool's error is replaced with `[secret]`, and a run whose output contains one fails with its output withheld.
## Budgets and usage [#budgets-and-usage]
Paid work an app starts is attributed to its install in the usage ledger, alongside the person it was done for. That includes work that runs later, such as a queued workflow run or a Relay task the app assigned.
An install's monthly budget is the one an admin set on its **Manage** page, or else the manifest's `budget.monthlyUsd`. Without either, spend is not capped. Before the app runs a workflow, assigns a Relay task, runs a code tool, calls a model, generates media, asks Chat for an answer, searches a knowledge base or runs an integration action, the workspace compares the spend since the start of the calendar month (UTC) with the budget. Once spend reaches it, those calls fail with `budget_exhausted` until the month ends or an admin raises the budget. Reads, row writes, file calls and opening Chat do not check the budget.
Model, media, Chat, knowledge and integration calls also have a per-install ceiling each minute, separate from the bridge's per-second limit, so one busy app cannot run up a large bill before its budget notices. Over it, calls fail with `rate_limited`.
A Chat answer is billed once, as a Chat turn, and its cost also counts against the app's budget.
The check happens before each call, so one call that starts below the budget can finish above it.
`agentserve app usage` and the **Manage** page show this month's spend against the budget, by source.
## Audit trail [#audit-trail]
Every change to an app is in the workspace audit trail: uploading, publishing and retiring versions, installing, updating, rolling back and uninstalling, setting and deleting secrets, and the rules an install creates.
Every write an app makes — adding rows, running or queuing a workflow, assigning a Relay task, creating a file — is recorded as an app write naming the app, the version and the method, in addition to the record the underlying action keeps. Every code tool run is recorded too, with the tool, who called it and from where, the status and the duration, but never its input, output or secrets. `agentserve app logs` and the **Manage** page show an install's trail.
Because the app acts as the person using it, those records name that person. Usage is billed the same way as their own work, and also counted against the app's budget.
---
# Templates (/developers/templates)
Every template is a complete React app built with Vite, with a manifest, the tables it needs, and a README that says what to change. Create one with:
```bash
agentserve app init my-app --template
```
`agentserve app init --list` prints the templates. Each one installs the tables it uses if they are missing, and always acts as the person using it.
## blank [#blank]
**For:** any app that none of the others fits.
A minimal starting point. It shows who is signed in and their role, which tables the app can read, and the rows of a small example table called `items`.
| Permission | Why |
| ----------------- | ------------------------ |
| `tables: [items]` | Reads the example table. |
```bash
agentserve app init my-app --template blank
```
## sales-cockpit [#sales-cockpit]
**For:** account managers preparing for a customer meeting.
Pick an account and see its key figures from the semantic model, its last few visits, and a button that opens Chat with a request for talking points. After the meeting, log the outcome; the person reviews it before it is saved.
| Permission | Why |
| ---------------------------- | --------------------------------------------------------------------------------- |
| `tables: [accounts, visits]` | Lists accounts, reads recent visits, and adds logged visits. |
| `model: read` | Reads account figures from the semantic model. |
| `chat: ask` | Opens Chat with a talking-points question. The person decides whether to send it. |
Change the measure and entity names in `src/lib/model.ts` to the ones in your model.
```bash
agentserve app init my-app --template sales-cockpit
```
## field-visit-logger [#field-visit-logger]
**For:** field teams who visit customers.
Log each visit with the customer, date, outcome and notes, and attach a photo. The notes are saved as a Markdown file and the photo as an image in the workspace's files, and the visit row keeps both file ids. From the visits list, hand any visit to a Relay for follow-up.
| Permission | Why |
| --------------------------- | ------------------------------------------------------------ |
| `tables: [visits]` | Lists visits and adds new ones. |
| `files: write` | Saves notes and photos, up to 5 MB each, as workspace files. |
| `relays: [Field follow-up]` | Gives the follow-up Relay a task for a visit. |
It declares the `Field follow-up` Relay and a monthly budget of $25 for its Relay tasks.
```bash
agentserve app init my-app --template field-visit-logger
```
## deal-desk [#deal-desk]
**For:** sales teams that check offers against pricing policy.
Enter an offer and see its score from 0 to 100 as you type, with the reasons: discount against the limit for the customer's tier, margin, contract term and deal size. Then request approval: the offer is saved as pending approval, and a Relay gets a task with the score and reasons. The score comes from `score_offer`, a [code tool](/developers/code-tools) the app ships: a plain TypeScript function that runs on Agent Serve, so the same offer always gets the same score, and Chat, Relays and workflows can ask it too.
| Permission | Why |
| ------------------------------- | ---------------------------------------- |
| `tables: [offers]` | Lists offers and saves new ones. |
| `relays: [Deal desk approvals]` | Sends each offer to the approvals Relay. |
It declares the `Deal desk approvals` Relay, the `score_offer` tool, and a monthly budget of $25. Code tools run from an installed version, so in `agentserve app dev` the score appears once you have deployed, published and installed the app.
```bash
agentserve app init my-app --template deal-desk
```
## support-triage [#support-triage]
**For:** support leads working through a queue.
Open tickets are listed most urgent and oldest first. Select one to read it, ask Chat to summarise it, and route it to a team by running your `Route ticket` workflow. The app shows whether the run completed or why it failed.
| Permission | Why |
| --------------------------- | ------------------------------------------------------------------------- |
| `tables: [tickets]` | Reads the open tickets. |
| `workflows: [Route ticket]` | Runs the routing workflow for a ticket. |
| `chat: ask` | Opens Chat with a summary request. The person decides whether to send it. |
You build and deploy the `Route ticket` workflow yourself; it takes `ticketId`, `team` and `priority`. The template sets a monthly budget of $25 for workflow runs.
```bash
agentserve app init my-app --template support-triage
```
## ops-exceptions [#ops-exceptions]
**For:** operations managers watching for problems.
The board shows late shipments by region and site from the semantic model and flags every site above a threshold as an exception. Click a row to see that site split by carrier, then create a follow-up task in the `ops_tasks` table.
| Permission | Why |
| --------------------- | ------------------------------------------------------------------------- |
| `model: read` | Reads late shipments by region, site and carrier from the semantic model. |
| `tables: [ops_tasks]` | Adds follow-up tasks. |
Change the measure, entity and attribute names in `src/lib/model.ts` to the ones in your model.
```bash
agentserve app init my-app --template ops-exceptions
```
## knowledge-assistant [#knowledge-assistant]
**For:** anyone who wants quick, cited answers from the team's documents.
Ask a question. The app searches a knowledge base for matching passages, then streams a short answer from a chat model that cites them, with the sources listed underneath. It shows the platform AI an app can call directly, without wrapping each step in a workflow.
| Permission | Why |
| --------------------------- | ----------------------------------------------------------------- |
| `knowledge: [Product docs]` | Searches the knowledge base for passages that match the question. |
| `ai.models: [gpt-5.5]` | Writes the answer from those passages, streamed as it is written. |
Set `KNOWLEDGE_BASE` and `ANSWER_MODEL` in `src/lib/answer.ts`, and the same names in the manifest. Searches and answers count against the app's budget, which the template sets to $20 a month.
```bash
agentserve app init my-app --template knowledge-assistant
```
`sales-cockpit` and `ops-exceptions` query the semantic model, and `field-visit-logger` and `deal-desk` assign Relay tasks. Neither call works in `agentserve app dev` yet, so try those parts in a deployed draft. See [agentserve app dev](/developers/cli#agentserve-app-dev).
The templates that declare a Relay create it as a draft on install, and only when installed from the **Apps** page in the workspace. Someone finishes the Relay's setup and publishes it before it can take tasks. See [Requires](/developers/manifest#relays).
## Example apps [#example-apps]
Two complete apps in the repository show what a finished app looks like: several pages,
tables the workspace already has, a table and Relays created on install, Chat, a code tool,
approvals, and loading, empty and error states throughout. They live in `examples/apps/`
and are checked like the templates: type-checked, built, manifest-validated and packed on
every change.
They read tables that are already in the workspace, and column names differ between
workspaces. So each one reads the tables' columns when it starts, maps the fields it needs
by name, and asks the person to **map your columns** when a required one is missing, instead
of failing.
### pipeline-cockpit [#pipeline-cockpit]
**For:** sales managers and reps working at-risk deals.
**Deals** ranks at-risk deals by the money at stake, how serious the risk is and how soon the deal is due, with filters by rep, stage and value. **Deal brief** shows one deal with its history and the account's other deals, opens Chat for talking points, logs the next step to a `deal_notes` table after a review screen, and assigns a follow-up to a Relay. **Team** shows each rep against quota with the at-risk value on their desk.
| Permission | Why |
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `tables: [deals_at_risk, pipeline_opportunities, rep_summary, deal_notes]` | Reads deals, history and the team; adds next steps. |
| `relays: [Pipeline follow-up]` | Assigns follow-ups. |
| `chat: ask` | Opens Chat with a talking-points question. The person decides whether to send it. |
### credit-desk [#credit-desk]
**For:** credit analysts and underwriters, and the teams that watch fraud and complaints.
**Applications** lists loans waiting for a credit review, largest exposure first. **Review** scores one loan with `score_credit_risk`, a [code tool](/developers/code-tools) the app ships, and lists every factor with the points it added; the reviewer then approves, refers or declines. A decline is saved as pending approval and sent to a Relay to approve. **Exceptions** lists fraud alerts under review and open complaints, each a click from a Relay task.
| Permission | Why |
| ------------------------------------------------------------- | --------------------------------------------------------- |
| `tables: [loans, fraud_alerts, complaints, credit_decisions]` | Reads the queue and exceptions; saves decisions. |
| `relays: [Credit approvals, Credit exceptions]` | Sends declines for approval and exceptions for follow-up. |
It declares the `score_credit_risk` tool and a monthly budget of $50.
### Deploy an example [#deploy-an-example]
Example apps are not offered by `app init`. From a checkout of the repository, copy one out
with the SDK vendored the same way `app init` does it, then deploy as usual:
```bash
bun run scaffold:example-app credit-desk ../credit-desk
cd ../credit-desk
bun install
agentserve app deploy
agentserve app publish 0.1.0
agentserve app install
```
Each example's README lists the tables it expects, the columns it looks for, and what to change.
---
# App (/cli/app)
Every command below also accepts the [global options](/cli/commands#global-options).
## Create a new app from a starter template [#create-a-new-app-from-a-starter-template]
```bash
agentserve app init [dir] [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | --------------------------- |
| `dir` | No | Folder to create the app in |
**Options**
| Option | Required | Description |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-t, --template ` | No | Starter template. Accepted values: `blank`, `sales-cockpit`, `field-visit-logger`, `deal-desk`, `support-triage`, `ops-exceptions`, `knowledge-assistant`. Defaults to `blank`. |
| `--name ` | No | App name in the manifest; defaults to the folder name. |
| `--title ` | No | App title people see; defaults to one made from the name. |
| `--force` | No | Add the app to a folder that already has other files. |
| `--list` | No | List the starter templates and exit. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Build the app and run it locally in the same sandboxed frame the workspace uses, with your own access [#build-the-app-and-run-it-locally-in-the-same-sandboxed-frame-the-workspace-uses-with-your-own-access]
```bash
agentserve app dev [dir] [options]
```
Build the app and run it locally in the same sandboxed frame the workspace uses, with your own access (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ----------- |
| `dir` | No | App folder |
**Options**
| Option | Required | Description |
| --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `-p, --port ` | No | Port for the local host page. Defaults to `5175`. |
| `--page ` | No | Page to open first. |
| `--launch ` | No | What launch.get answers, as a plugin command or action opens the page: \{"kind":"command","name":"brief","args":\{"file":"q3"}}. |
| `--build-command ` | No | Command that builds the app (default: \ run build). |
| `--no-build` | No | Do not build; serve the existing output and reload when it changes. |
| `--no-open` | No | Do not open a browser. |
## Build the app, pack it and upload it as a draft version [#build-the-app-pack-it-and-upload-it-as-a-draft-version]
```bash
agentserve app deploy [dir] [options]
```
Build the app, pack it and upload it as a draft version (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ----------- |
| `dir` | No | App folder |
**Options**
| Option | Required | Description |
| --------------------------- | -------- | -------------------------------------------------------------------- |
| `--build-command ` | No | Command that builds the app (default: \ run build). |
| `--no-build` | No | Upload the existing build output without building. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Publish a draft version so workspaces can install it (admin) [#publish-a-draft-version-so-workspaces-can-install-it-admin]
```bash
agentserve app publish [options]
```
Publish a draft version so workspaces can install it (admin) (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| --------- | -------- | -------------------------------------------- |
| `version` | Yes | Version to publish, such as 1.2.0, or its id |
**Options**
| Option | Required | Description |
| -------------------- | -------- | ------------------------------------------------------------- |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `--rollout` | No | Also move this workspace's install to the version. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Move this workspace's install to an earlier published version (admin) [#move-this-workspaces-install-to-an-earlier-published-version-admin]
```bash
agentserve app rollback [options]
```
Move this workspace's install to an earlier published version (admin) (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| --------- | -------- | -------------------------------------------------- |
| `version` | Yes | Published version to run, such as 1.1.0, or its id |
**Options**
| Option | Required | Description |
| -------------------- | -------- | ------------------------------------------------------------- |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Install a published app in the workspace (admin); pick it with -w, --workspace [#install-a-published-app-in-the-workspace-admin-pick-it-with--w---workspace]
```bash
agentserve app install [options]
```
Install a published app in the workspace (admin); pick it with -w, --workspace (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------------------------------------------- |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `--to-version ` | No | Published version to install; defaults to the highest. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Show the install's audit trail, oldest first; --follow keeps watching [#show-the-installs-audit-trail-oldest-first---follow-keeps-watching]
```bash
agentserve app logs [options]
```
Show the install's audit trail, oldest first; --follow keeps watching (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------- |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `-f, --follow` | No | Keep polling for new entries until Ctrl-C. |
| `--interval ` | No | Seconds between polls with --follow. Defaults to `5`. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Show this month's spend against the app's budget, by source [#show-this-months-spend-against-the-apps-budget-by-source]
```bash
agentserve app usage [options]
```
Show this month's spend against the app's budget, by source (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| -------------------- | -------- | ------------------------------------------------------------- |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Show what the app may reach, installed and in the local manifest [#show-what-the-app-may-reach-installed-and-in-the-local-manifest]
```bash
agentserve app permissions [dir] [options]
```
Show what the app may reach, installed and in the local manifest (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ------------------------------------ |
| `dir` | No | App folder whose manifest to compare |
**Options**
| Option | Required | Description |
| -------------------- | -------- | ------------------------------------------------------------- |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Set a secret; prompts without echo when --value is not given [#set-a-secret-prompts-without-echo-when---value-is-not-given]
```bash
agentserve app secrets set [options]
```
Set a secret; prompts without echo when --value is not given (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ------------------------------- |
| `name` | Yes | Secret name, such as API\_TOKEN |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | --------------------------------------------------------------------------------------------- |
| `--value ` | No | Secret value. Inline values land in shell history; @path reads a file and @- stdin, verbatim. |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## List secret names and when each was set [#list-secret-names-and-when-each-was-set]
```bash
agentserve app secrets list [options]
```
List secret names and when each was set (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| -------------------- | -------- | ------------------------------------------------------------- |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
## Delete a secret [#delete-a-secret]
```bash
agentserve app secrets delete [options]
```
Delete a secret (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ----------- |
| `name` | Yes | Secret name |
**Options**
| Option | Required | Description |
| -------------------- | -------- | ------------------------------------------------------------- |
| `--app ` | No | App to act on; defaults to the name in ./agentserve.app.yaml. |
| `--json` | No | Print machine-readable JSON (same as --output json). |
---
# Apps (/cli/apps)
Every command below also accepts the [global options](/cli/commands#global-options).
## Delete app secret [#delete-app-secret]
```bash
agentserve apps secrets delete [options]
```
Delete App Secret (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `installId` | Yes | Install identifier. |
| `name` | Yes | Secret name. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## List app secrets [#list-app-secrets]
```bash
agentserve apps secrets list
```
List App Secrets (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `installId` | Yes | Install identifier. |
## Set app secret [#set-app-secret]
```bash
agentserve apps secrets update [options]
```
Set App Secret (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `installId` | Yes | Install identifier. |
| `name` | Yes | Secret name. |
**Options**
| Option | Required | Description |
| ----------------- | -------- | ---------------------------------------------- |
| `--value ` | Yes | Write-only secret value. It is never returned. |
## Get app [#get-app]
```bash
agentserve apps get
```
Get App (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | --------------- |
| `appId` | Yes | App identifier. |
## Get app deployment [#get-app-deployment]
```bash
agentserve apps deployments get
```
Get App Deployment (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ---------------------- |
| `appId` | Yes | App identifier. |
| `deploymentId` | Yes | Deployment identifier. |
## List app deployments [#list-app-deployments]
```bash
agentserve apps deployments list
```
List App Deployments (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | --------------- |
| `appId` | Yes | App identifier. |
## Publish app deployment [#publish-app-deployment]
```bash
agentserve apps deployments publish
```
Publish App Deployment (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ---------------------- |
| `appId` | Yes | App identifier. |
| `deploymentId` | Yes | Deployment identifier. |
## Get app install [#get-app-install]
```bash
agentserve apps installs get
```
Get App Install (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `installId` | Yes | Install identifier. |
## Install app [#install-app]
```bash
agentserve apps installs create [options]
```
Install App (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------------- |
| `--app-id ` | Yes | App to install. |
| `--version-id ` | No | A published version; omit to install the highest published version. |
## List app installs [#list-app-installs]
```bash
agentserve apps installs list
```
List App Installs (OAuth login or personal API key required)
## Uninstall app [#uninstall-app]
```bash
agentserve apps installs delete [options]
```
Uninstall App (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `installId` | Yes | Install identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Update app install [#update-app-install]
```bash
agentserve apps installs update [options]
```
Update App Install (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `installId` | Yes | Install identifier. |
**Options**
| Option | Required | Description |
| ------------------------------------- | -------- | ------------------------------------------------------------------------------------- |
| `--version-id ` | No | A published version to run instead — an upgrade or a roll back. |
| `--pinned-for ` | No | Who sees the app as its own rail icon. (JSON, or @path / @- to read a file or stdin). |
| `--budget-monthly-usd ` | No | Monthly budget in USD; null falls back to the manifest budget. |
| `--status ` | No | Turn the app on or off. Accepted values: `active`, `disabled`. |
## Get app install activity [#get-app-install-activity]
```bash
agentserve apps activity list
```
Get App Install Activity (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `installId` | Yes | Install identifier. |
## List app repositories [#list-app-repositories]
```bash
agentserve apps repos list
```
List App Repositories (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | --------------- |
| `appId` | Yes | App identifier. |
## List apps [#list-apps]
```bash
agentserve apps list
```
List Apps (OAuth login or personal API key required)
## List app tools [#list-app-tools]
```bash
agentserve apps tools list [options]
```
List App Tools (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ------------------------------- |
| `--install-id ` | No | Only the tools of this install. |
## Publish app version [#publish-app-version]
```bash
agentserve apps publish create [options]
```
Publish App Version (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `appId` | Yes | App identifier. |
| `versionId` | Yes | Version identifier. |
**Options**
| Option | Required | Description |
| -------------- | -------- | ------------------------------------------------------------- |
| `--rollout` | No | Also move this workspace's install of the app to the version. |
| `--no-rollout` | No | Send --rollout as false. |
## Rebuild app deployment [#rebuild-app-deployment]
```bash
agentserve apps rebuild create
```
Rebuild App Deployment (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ---------------------- |
| `appId` | Yes | App identifier. |
| `deploymentId` | Yes | Deployment identifier. |
## Retire app version [#retire-app-version]
```bash
agentserve apps retire create [options]
```
Retire App Version (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `appId` | Yes | App identifier. |
| `versionId` | Yes | Version identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Run app tool [#run-app-tool]
```bash
agentserve apps run create [options]
```
Run App Tool (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | ------------------- |
| `installId` | Yes | Install identifier. |
| `toolName` | Yes | Tool name. |
**Options**
| Option | Required | Description |
| ----------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `--input ` | No | The tool's input; validated against its input schema. (JSON, or @path / @- to read a file or stdin). |
## Upload app version [#upload-app-version]
```bash
agentserve apps versions create
```
Upload App Version (OAuth login or personal API key required)
---
# Audit Logs (/cli/audit-logs)
`agentserve audit-logs` is also spelled `agentserve audit-log`.
Every command below also accepts the [global options](/cli/commands#global-options).
## Get audit log [#get-audit-log]
```bash
agentserve audit-logs get [options]
```
Get Audit Log (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ------------ | -------- | --------------------------- |
| `auditLogId` | Yes | Audit-log entry identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | No | Organization ID; defaults to your only organization, and is required when your account belongs to more than one (OAuth login or personal API key required). |
## List audit logs [#list-audit-logs]
```bash
agentserve audit-logs list [options]
```
List Audit Logs (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--action ` | No | Filter by exact action name. |
| `--resource-type ` | No | Filter by resource type. Accepts a comma-separated set; members are trimmed and deduplicated, and member order affects neither the result nor the cursor. |
| `--resource-id ` | No | Filter by exact resource identifier. |
| `--start-date ` | No | Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--end-date ` | No | Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--include-departed` | No | Include actions by users who have left the organization. |
| `--no-include-departed` | No | Send --include-departed as false. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
| `--organization ` | No | Organization ID; defaults to your only organization, and is required when your account belongs to more than one (OAuth login or personal API key required). |
| `--actor-email ` | No | Filter by actor email address. |
| `--all-workspaces` | No | Do not filter to the configured workspace (OAuth login or personal API key required for account-wide access). |
---
# Authentication (/cli/authentication)
`agentserve login` signs you in through your browser. It prefers OAuth, which stores a
short-lived login that renews itself, and selects API-key pairing for remote
terminals or servers without OAuth support. In CI you supply an existing API
key through the environment instead.
## Signing in [#signing-in]
```bash
agentserve login
```
Choose a method explicitly when the credential type matters:
```bash
agentserve login --method oauth
agentserve login --method api-key
```
`--method oauth` requires a server with OAuth support and authentication enabled;
it never falls back to an API key. Explicit OAuth selection also overrides
SSH/headless detection; your browser still needs to reach the CLI's
local callback. `--method api-key` uses pairing-code approval to create a new
permanent API key. To supply an existing key, set `AGENT_SERVE_API_KEY` instead.
API-key pairing requires a server that supports `platform` API keys. Upgrade
older deployments that only issue `copilot` keys before starting login; those
keys cannot authenticate the platform CLI.
OAuth login opens your browser on Agent Serve's sign-in page, then on a consent page that
names the Agent Serve CLI and what it will be able to do. Approve, and the browser hands
control back to the terminal:
```
Signing in to https://agentserve.valnoraelric.com as profile default
https://agentserve.valnoraelric.com/api/auth/oauth2/authorize?client_id=agentserve-cli&…
Waiting for you to approve in the browser…
✓ Logged in. Login stored in /Users/you/.agent-serve/credentials
Renews itself; revoke it any time in Settings → General → Authorized apps, or with: agentserve logout
No default workspace. Set one with: agentserve configure --set-workspace
```
This is the OAuth 2.0 authorization-code flow with PKCE and a loopback redirect,
aligned with current OAuth security guidance. The browser only ever carries a one-time code;
the tokens are exchanged over the terminal's own connection and written to
`~/.agent-serve/credentials` with `0600` permissions. Access tokens last an hour and are
renewed automatically from a refresh token. The complete login has a fixed
30-day lifetime; after it expires, run `agentserve logout`, then sign in again.
Only approve a consent page you reached by running `agentserve login` yourself. A
consent page that appears unprompted, or one you were sent a link to, is not
your login.
| Option | What it does |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `--method ` | `oauth` requires OAuth login; `api-key` creates a permanent key through pairing. Auto-selects when omitted |
| `--no-browser` | Print the approval URL without opening it; works with either method |
| `--read-only` | Ask only for permission to read, never to change anything |
| `--callback-port ` | Pin the loopback callback port, primarily for an SSH session that forwards the same fixed port |
| `-y, --yes` | Overwrite an existing API-key profile without prompting |
### Over SSH or in a container [#over-ssh-or-in-a-container]
OAuth login needs your browser to reach a listener on the machine running
`agentserve`. When it cannot — an SSH session, a dev container, a remote box — use the
API-key pairing flow. The CLI selects it automatically in an SSH session when
no method or callback port is specified:
```bash
agentserve login --method api-key --no-browser
```
The terminal prints a pairing code and a URL you can open on any device:
```
Signing in to https://agentserve.valnoraelric.com as profile default
Pairing code: K7M2-P9XT
Confirm this code matches what the browser shows before approving.
https://agentserve.valnoraelric.com/cli/auth?request=…&scope=platform
Waiting for approval…
✓ Logged in. Key stored in /Users/you/.agent-serve/credentials
Personal key, defaulting to 2f6d0b1c-8a34-4d92-b7e5-31c8a0f45d67. Override per command with --workspace.
```
Confirm the pairing code in the browser matches the one in your terminal before
approving. That check is what binds the approval to your terminal.
The handoff issues a permanent personal API key rather than a renewing login,
so revoke it under **Settings → API keys** when you are done with that machine.
It also works when OAuth is unavailable or switched off, provided the server
supports platform API-key pairing. When `--method` is omitted, the CLI checks
OAuth availability and selects pairing if unavailable; that discovery does not
verify pairing compatibility. An explicit `--method oauth` fails in that case.
`--read-only` and `--callback-port` belong to OAuth login and have no
meaning here, so combining either with the handoff stops the login rather than
storing a credential you did not ask for. If your SSH session forwards a port
from the remote loopback interface to the browser's machine, use
`--method oauth --callback-port ` with that port. An ordinary container port publication
cannot reach a listener bound to the container's own loopback interface; use
`--method api-key` there.
### Picking a workspace [#picking-a-workspace]
A normal login can act across every workspace you belong to; `--read-only`
limits it to read operations. The profile's `workspace` setting only decides the default target.
Set it after signing in, or pass `--workspace` per command:
```bash
agentserve workspaces list
agentserve configure --set-workspace 2f6d0b1c-8a34-4d92-b7e5-31c8a0f45d67
agentserve workflows list --workspace 9b4c7e02-1d58-4f36-a0c9-6e2b85df413a
```
With the pairing-code handoff you choose the default workspace on the approval
page instead.
To target another workspace without a second login, add a workspace profile:
```bash
agentserve profile add acme --workspace 7e2d9c14-6b83-4a55-8f01-c4d3e9a76b28
agentserve --profile acme whoami
```
The new profile stores `auth_profile = default` and its own workspace. Omit
`--workspace` in an interactive terminal to choose from the workspaces your
login can access; scripts must provide the workspace ID explicitly. The picker
is capped at 1,000 entries and asks for an explicit ID above that.
## Checking who you are [#checking-who-you-are]
```bash
agentserve whoami # resolved settings, plus a live check that they work
agentserve whoami --no-verify # resolved settings only, no request
```
Prints the resolved endpoint, workspace, and output format, which source each
value came from, and whether the profile holds an OAuth login or an API key,
then reads the configured workspace to prove the credential is accepted and can
reach it.
It exits `0` when the check passes, `1` when the credentials are wrong, and `2`
when the check could not be made at all — no workspace to check against, or an
endpoint that did not answer. The split matters in CI: only `1` is fixed by
logging in again.
## Signing out [#signing-out]
```bash
agentserve logout # sign out of Agent Serve and remove the stored login
agentserve logout --all # remove the profile entirely, including its settings
```
For an OAuth login, `agentserve logout` revokes that login's complete token family
before removing it from disk, including access tokens issued before earlier
rotations. Other machines that ran their own `agentserve login` remain signed in. To
cut off every independent login for the client, revoke the grant under
**Settings → General → Authorized apps**.
A workspace profile that shares authentication cannot remove the shared login.
Remove only that local profile with `agentserve logout --all --profile `, or log
out of the authentication profile named by the error message. Removing an
authentication profile entirely is refused until its workspace profiles are
removed, so it cannot leave dangling references.
For a login created with `--method api-key`, `agentserve logout` removes the API key from
disk but does **not** revoke it. Revoke keys under **Settings → API keys**.
## Authenticating CI [#authenticating-ci]
Set an API key and workspace in the environment; no saved login is required,
and the environment key overrides any stored login:
```bash
export AGENT_SERVE_API_KEY="sk-as-…"
export AGENT_SERVE_WORKSPACE="2f6d0b1c-8a34-4d92-b7e5-31c8a0f45d67"
agentserve workflows run 3a9e21d8-5f47-4c0b-b2ea-91d7c6034ef8 --input '{"source":"nightly"}' --output json
```
Create the key in Agent Serve under **Settings → API keys**. Store it as a secret in your
CI provider — never commit it.
`AGENT_SERVE_CONFIG_DIR` relocates both files if you need them somewhere other than
`~/.agent-serve`, such as a runner with no writable home directory.
### GitHub Actions [#github-actions]
```yaml title=".github/workflows/nightly.yml"
jobs:
digest:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install -g agentserve
- run: agentserve workflows run 3a9e21d8-5f47-4c0b-b2ea-91d7c6034ef8 --output json
env:
AGENT_SERVE_API_KEY: ${{ secrets.AGENT_SERVE_API_KEY }}
AGENT_SERVE_WORKSPACE: ${{ vars.AGENT_SERVE_WORKSPACE }}
```
## Several accounts and workspaces [#several-accounts-and-workspaces]
Use separate logins for separate identities or deployments:
```bash
agentserve login --profile dev --endpoint http://localhost:3000
agentserve login --profile prod
agentserve workflows list --profile dev
agentserve workflows list --profile prod
```
Use workspace profiles when one login should target several workspaces:
```bash
agentserve profile add marketing --workspace c3a70e58-9f21-4d6b-b842-05e7f19c6a3d
agentserve profile add support --workspace e0d94b17-3c62-45af-9718-b6a2c8035f4e
agentserve workflows list --profile marketing
agentserve workflows list --profile support
```
See [Configuration](/cli/configuration) for how profiles are stored and resolved.
## Self-hosted and non-production deployments [#self-hosted-and-non-production-deployments]
Point the CLI at any deployment with `--endpoint`, then sign in against it:
```bash
agentserve login --profile local --endpoint http://localhost:3000
```
Save it to avoid repeating the flag:
```bash
agentserve configure --set-endpoint http://localhost:3000 --profile local
```
OAuth sign-in is available by default when server authentication is enabled.
`DISABLE_AUTH=true` disables OAuth. Servers without OAuth use the pairing-code
handoff when they support platform API keys. See
[Sign in with Agent Serve](/platform/self-hosting/authentication#sign-in-with-agent-serve) for
server configuration and upgrade requirements.
## Where the login is stored [#where-the-login-is-stored]
Logins live in `~/.agent-serve/credentials`, written `0600`, separate from the non-secret
`~/.agent-serve/config`. Commit `config` to a dotfiles repo if you like; never
`credentials`.
```ini title="~/.agent-serve/credentials"
[default]
access_token = as_oat_…
refresh_token = as_ort_…
token_expires_at = 1788547200000
oauth_issuer = https://agentserve.valnoraelric.com/api/auth
oauth_login_id = …
oauth_scope = offline_access api:read api:write
[ci-box]
api_key = sk-as-…
```
A profile holds one login. A stored API key can be replaced after confirmation
or with `--yes`; a live OAuth login must be revoked with `agentserve logout` before
signing in again. Several `agentserve` commands running at once share one renewal, so
a parallel shell loop cannot sign itself out.
Run `agentserve login` separately on each machine. Copying `~/.agent-serve/credentials` copies
one single-use refresh-token family; simultaneous use from both copies is
treated as token replay and revokes that login. If a refresh response is lost
because the process or connection stops, the CLI does not retry the consumed
token: run `agentserve logout`, then `agentserve login` again. This fail-closed behavior keeps
a copied token from surviving an ambiguous refresh.
## Organization audit logs [#organization-audit-logs]
`agentserve audit-logs` requires a **personal** credential — an OAuth login, or the
personal API key `agentserve login --method api-key` issues. A workspace-scoped key cannot
read organization-level audit logs.
---
# Billing (/cli/billing)
Every command below also accepts the [global options](/cli/commands#global-options).
## Show billing status, credits spent this period, and storage used [#show-billing-status-credits-spent-this-period-and-storage-used]
```bash
agentserve billing status [options]
```
Show billing status, credits spent this period, and storage used (credits and storage require an OAuth login or personal API key)
**Options**
| Option | Required | Description |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------- |
| `--all-workspaces` | No | Do not filter to the configured workspace (OAuth login or personal API key required for account-wide access). |
## List credit usage events [#list-credit-usage-events]
```bash
agentserve billing logs [options]
```
List credit usage events (an OAuth login or personal API key reports only your events; a workspace API key reports every member's in aggregate, unattributed)
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--source ` | No | Filter by usage source; agentserve-chat covers Chat in the workspace and in the editor. Accepted values: `workflow`, `wand`, `agentserve-chat`, `mcp_copilot`, `mothership_block`, `knowledge-base`, `voice-input`, `enrichment`, `voice-output`, `api-tool`, `dataset-refresh`, `dataset-query`, `voice-mode`, `app-tool`, `app-build`, `app-ide`, `app-ai`, `slides`. |
| `--period ` | No | Billing period. Accepted values: `1d`, `7d`, `30d`, `all`, `custom`. |
| `--start-date ` | No | Custom period start (ISO 8601). |
| `--end-date ` | No | Custom period end (ISO 8601). |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
| `--all-workspaces` | No | Do not filter to the configured workspace (OAuth login or personal API key required for account-wide access). |
---
# Blocks (/cli/blocks)
Every command below also accepts the [global options](/cli/commands#global-options).
## Get block [#get-block]
```bash
agentserve blocks get
```
**Arguments**
| Argument | Required | Description |
| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `blockId` | Yes | Block type identifier. An unversioned base type resolves to the newest version, and the response echoes the resolved id. |
## List blocks [#list-blocks]
```bash
agentserve blocks list [options]
```
**Options**
| Option | Required | Description |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--search ` | No | Case-insensitive substring match against the block id, name, and description. |
| `--category ` | No | Restrict to one toolbar category. Accepted values: `blocks`, `tools`, `triggers`. |
| `--capability ` | No | Restrict to blocks that can start a workflow — the `triggers` category, blocks declaring `triggerAllowed`, and blocks with trigger-mode fields. Accepted values: `trigger`. |
| `--source ` | No | Restrict to built-in blocks or this workspace's deployed custom blocks. Accepted values: `builtin`, `custom`. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `id`, `name`, `category`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
---
# Chat Deployments (/cli/chat-deployments)
Every command below also accepts the [global options](/cli/commands#global-options).
## List chat deployments [#list-chat-deployments]
```bash
agentserve chat-deployments list [options]
```
**Options**
| Option | Required | Description |
| ----------------------- | -------- | --------------------------------------------------------------------------------------- |
| `--workflow-id ` | No | Restrict to deployments of one workflow. |
| `--is-active` | No | Restrict to active or inactive deployments. |
| `--no-is-active` | No | Send --is-active as false. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `identifier`, `createdAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
---
# CLI (/cli/cli)
Every command below also accepts the [global options](/cli/commands#global-options).
## Find the command for a task, described in plain words [#find-the-command-for-a-task-described-in-plain-words]
```bash
agentserve cli search
```
**Arguments**
| Argument | Required | Description |
| ---------- | -------- | ------------------------------------------------- |
| `query...` | Yes | What you want to do, e.g. "cancel a workflow run" |
---
# CLI Commands (/cli/commands)
Every `agentserve` command follows the same shape:
```bash
agentserve [sub-resource] [arguments] [options]
```
Some resource groups also accept a singular alias: `agentserve workflow get` and
`agentserve workflows get` are the same command. `knowledge` also answers to `kb`.
Each group’s reference lists its supported aliases.
## Global options [#global-options]
These apply to every command, and may be written before or after it.
| Option | Description |
| ---------------------- | --------------------------------------------------------------------------------- |
| `-P, --profile ` | Profile to use (env: AGENT\_SERVE\_PROFILE). |
| `--endpoint ` | Deployment to talk to (env: AGENT\_SERVE\_BASE\_URL). |
| `-w, --workspace ` | Workspace to target (env: AGENT\_SERVE\_WORKSPACE). |
| `--output ` | Output format for this command. Accepted values: `table`, `json`, `yaml`, `text`. |
## Command groups [#command-groups]
| Group | Description |
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| [`agentserve profiles`](/cli/profiles) | List profiles or add a workspace profile that shares a stored login |
| [`agentserve telemetry`](/cli/telemetry) | Control anonymous usage reporting |
| [`agentserve runner`](/cli/runner) | Run Agent Serve jobs on this machine (link, run, status, unlink) |
| [`agentserve cli`](/cli/cli) | Discover commands in this CLI |
| [`agentserve model`](/cli/model) | Work with the semantic model as code (agentserve.model.yaml) |
| [`agentserve app`](/cli/app) | Build, run and ship apps that run inside a workspace (agentserve.app.yaml) (OAuth login or personal API key required) |
| [`agentserve apps`](/cli/apps) | Manage apps |
| [`agentserve audit-logs`](/cli/audit-logs) | Manage audit logs |
| [`agentserve billing`](/cli/billing) | Manage billing |
| [`agentserve blocks`](/cli/blocks) | Manage blocks |
| [`agentserve chat-deployments`](/cli/chat-deployments) | Manage chat deployments |
| [`agentserve connector-types`](/cli/connector-types) | Manage connector types |
| [`agentserve credentials`](/cli/credentials) | Manage credentials |
| [`agentserve custom-tools`](/cli/custom-tools) | Manage custom tools |
| [`agentserve files`](/cli/files) | Manage files |
| [`agentserve knowledge`](/cli/knowledge) | Manage knowledge |
| [`agentserve logs`](/cli/logs) | Manage logs |
| [`agentserve mcp-servers`](/cli/mcp-servers) | Manage mcp servers |
| [`agentserve meta`](/cli/meta) | Manage meta |
| [`agentserve organizations`](/cli/organizations) | Manage organizations |
| [`agentserve permission-groups`](/cli/permission-groups) | Manage permission groups |
| [`agentserve relays`](/cli/relays) | Manage relays |
| [`agentserve sandboxes`](/cli/sandboxes) | Manage sandboxes |
| [`agentserve secrets`](/cli/secrets) | Manage secrets |
| [`agentserve selectors`](/cli/selectors) | Manage selectors |
| [`agentserve semantic-model`](/cli/semantic-model) | Manage semantic model |
| [`agentserve skills`](/cli/skills) | Manage skills |
| [`agentserve slides`](/cli/slides) | Manage slides |
| [`agentserve spaces`](/cli/spaces) | Manage spaces |
| [`agentserve tables`](/cli/tables) | Manage tables |
| [`agentserve tools`](/cli/tools) | Manage tools |
| [`agentserve workflow-mcp-servers`](/cli/workflow-mcp-servers) | Manage workflow mcp servers |
| [`agentserve workflows`](/cli/workflows) | Manage workflows |
| [`agentserve workspaces`](/cli/workspaces) | Manage workspaces |
## Log in through the browser and store the login for the profile [#log-in-through-the-browser-and-store-the-login-for-the-profile]
```bash
agentserve login [options]
```
**Options**
| Option | Required | Description |
| ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--method ` | No | Credential to obtain: oauth requires OAuth support; api-key creates a permanent key through pairing (auto-selects when omitted). Accepted values: `oauth`, `api-key`. |
| `--no-browser` | No | Print the approval URL without opening it (either login method). |
| `--read-only` | No | Ask only for permission to read, never to change anything. |
| `--callback-port ` | No | Pin the local port the browser returns to. |
| `-y, --yes` | No | Overwrite an existing API-key profile without prompting. |
## Sign out and remove the profile's stored login [#sign-out-and-remove-the-profiles-stored-login]
```bash
agentserve logout [options]
```
**Options**
| Option | Required | Description |
| ------- | -------- | ---------------------------------------------------- |
| `--all` | No | Remove the profile entirely, including its settings. |
## Show the resolved profile, where each setting came from, and whether it works [#show-the-resolved-profile-where-each-setting-came-from-and-whether-it-works]
```bash
agentserve whoami [options]
```
**Options**
| Option | Required | Description |
| ------------- | -------- | -------------------------------------------------------- |
| `--no-verify` | No | Skip the API check and only print the resolved settings. |
## Switch a profile's default workspace, or set its endpoint or output format [#switch-a-profiles-default-workspace-or-set-its-endpoint-or-output-format]
```bash
agentserve configure [options]
```
**Options**
| Option | Required | Description |
| ----------------------- | -------- | ------------------------------------------------------ |
| `--set-endpoint ` | No | Agent Serve deployment to talk to. |
| `--set-workspace ` | No | Default workspace for workspace-scoped commands. |
| `--set-output ` | No | Default output format (table \| json \| yaml \| text). |
| `--unset ` | No | Remove settings (endpoint, workspace, output). |
## Update this global CLI installation to the newest release on its channel [#update-this-global-cli-installation-to-the-newest-release-on-its-channel]
```bash
agentserve update [options]
```
**Options**
| Option | Required | Description |
| ----------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `--package-manager ` | No | Package manager that installed this copy. Accepted values: `npm`, `pnpm`, `bun`, `yarn`. |
## Ask agent serve and print the reply [#ask-agent-serve-and-print-the-reply]
```bash
agentserve chat [options]
```
**Arguments**
| Argument | Required | Description |
| --------- | -------- | ----------------------- |
| `message` | Yes | What to ask Agent Serve |
**Options**
| Option | Required | Description |
| ------------------------- | -------- | --------------------------------------- |
| `-c, --conversation ` | No | Continue the conversation with this ID. |
---
# Configuration (/cli/configuration)
The CLI resolves an **endpoint**, **credential**, **workspace**, and **output
format**. The credential can be a stored OAuth login or an API key. Each resolves independently, so a saved default can still be overridden
for a single command.
## Profiles [#profiles]
A profile selects one set of defaults, in the style of the AWS CLI. It normally
uses its same-named stored identity, but a workspace profile can share another
profile's identity through `auth_profile`. Select one with `-P`, `--profile`, or
`AGENT_SERVE_PROFILE`:
```bash
agentserve workflows list --profile dev
AGENT_SERVE_PROFILE=dev agentserve workflows list
```
The profile is named `default` when you do not pick one.
```bash
agentserve profiles # list them; * marks the active one
```
Add a profile for another workspace without creating or copying an API key:
```bash
agentserve profile add acme --workspace 7e2d9c14-6b83-4a55-8f01-c4d3e9a76b28
```
## Setting defaults [#setting-defaults]
```bash
agentserve configure --set-endpoint http://localhost:3000 --profile dev
agentserve configure --set-workspace 5c81f3a6-0e27-4b94-8d15-a7f60c39b2e8 --profile dev
agentserve configure --set-output table
```
| Option | What it sets |
| ----------------------- | --------------------------------------------------------- |
| `--set-endpoint ` | The Agent Serve deployment to talk to |
| `--set-workspace ` | Default workspace for workspace-scoped commands |
| `--set-output ` | Default output format: `table`, `json`, `yaml`, or `text` |
| `--unset ` | Remove settings — `endpoint`, `workspace`, or `output` |
Run `agentserve configure` with no flags to print the profile's stored settings.
API keys are not settable here. Use [`agentserve login`](/cli/authentication), or
`AGENT_SERVE_API_KEY` for CI.
## Where settings come from [#where-settings-come-from]
Each setting resolves independently, and the first match wins:
| Rank | Source |
| ---- | ------------------------------------------------------------------------------------------------------------------ |
| 1 | Command-line flag — `--endpoint`, `--workspace`, `--output` |
| 2 | Environment — `AGENT_SERVE_ENDPOINT`, `AGENT_SERVE_API_KEY`, `AGENT_SERVE_WORKSPACE`, `AGENT_SERVE_OUTPUT` |
| 3 | `~/.agent-serve/config` for the selected profile and `~/.agent-serve/credentials` for its `auth_profile`, when set |
| 4 | Built-in default — `https://agentserve.valnoraelric.com` and `json` |
`agentserve whoami` prints the winning source for each setting:
```bash
agentserve whoami
```
## The files [#the-files]
Non-secret settings live in `~/.agent-serve/config`. It is safe to commit to a dotfiles
repo:
```ini title="~/.agent-serve/config"
[default]
endpoint = https://agentserve.valnoraelric.com
workspace = 2f6d0b1c-8a34-4d92-b7e5-31c8a0f45d67
output = table
[profile dev]
endpoint = http://localhost:3000
workspace = 5c81f3a6-0e27-4b94-8d15-a7f60c39b2e8
[profile acme]
auth_profile = default
workspace = 7e2d9c14-6b83-4a55-8f01-c4d3e9a76b28
```
Keys live in `~/.agent-serve/credentials`, written `0600`:
```ini title="~/.agent-serve/credentials"
[default]
api_key = sk-as-…
[dev]
api_key = sk-as-…
```
Section naming follows the AWS convention: `[profile dev]` in config, `[dev]` in
credentials. The `default` profile is `[default]` in both.
`auth_profile` references one direct profile and shares its endpoint and stored
login, whether OAuth or an API key. Workspace and output remain local.
References cannot be chained, and a shared profile cannot also set its own
endpoint or stored login.
## Environment variables [#environment-variables]
| Variable | Effect |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `AGENT_SERVE_PROFILE` | Profile to use |
| `AGENT_SERVE_ENDPOINT` | Deployment to talk to |
| `AGENT_SERVE_API_KEY` | API key — skips `agentserve login` entirely |
| `AGENT_SERVE_WORKSPACE` | Workspace to target |
| `AGENT_SERVE_OUTPUT` | Output format |
| `AGENT_SERVE_CONFIG_DIR` | Relocate the config directory and update cache; file-specific overrides below still win |
| `AGENT_SERVE_CONFIG_FILE` | Relocate only the config file |
| `AGENT_SERVE_CREDENTIALS_FILE` | Relocate only the credentials file |
| `AGENT_SERVE_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely. Defaults to `3600`, above every timeout the server itself applies |
| `AGENT_SERVE_DEBUG` | Trace each request's method, URL, status and duration to stderr |
| `AGENT_SERVE_NO_UPDATE_CHECK` | Turn off update checks and notices |
| `AGENT_SERVE_TELEMETRY_DISABLED` | Turn off anonymous usage reporting; see [usage data](/cli/usage-data) |
## Updates [#updates]
The CLI checks for a newer stable release at most once per day on eligible
interactive invocations. It prints an optional notice to stderr and continues
your command. Installation only happens when you run `agentserve update`.
Run `agentserve update` to update immediately, including in CI and when automatic checks
are disabled. It requires a global installation and verifies that the package
manager targets the running copy before installing. Supported managers are npm,
pnpm, Bun, and Yarn Classic. For custom installations, select the manager with
`agentserve update --package-manager bun`. Project-local and temporary package-runner
copies must be updated through their package manager.
Manual updates preserve the stable, staging, or dev release channel. Installation
failures stop with an error; concurrent update attempts are refused. Installer
output goes to stderr and does not mix with JSON output on stdout.
The updater resolves the channel through the selected package manager before
installing. Older registry or mirror releases are refused; a newer release is
installed by its exact version so a moving tag cannot change the target.
Checks are skipped in CI, when stderr is redirected, under `npm exec` or `npx`,
from a repository checkout, and for prerelease versions. Set
`AGENT_SERVE_NO_UPDATE_CHECK=1` to disable them.
The check uses `registry.npmjs.org` unless `npm_config_registry` names another
HTTP(S) registry. It sends no Agent Serve API key, workspace, or command. Query-string
credentials in a configured registry URL are preserved; URLs containing
username/password userinfo are rejected. Empty registry values use npm, while
malformed non-empty values disable the check. Redirects are not followed.
The daily cache is `~/.agent-serve/update-check.json`, or under `AGENT_SERVE_CONFIG_DIR`.
`AGENT_SERVE_CONFIG_FILE` and `AGENT_SERVE_CREDENTIALS_FILE` do not relocate it. If the cache
cannot be written, eligible invocations may check again. Concurrent commands
can also each check. Registry checks have a one-second deadline; package-manager
installation has a five-minute deadline. Registry-check failures suppress the
check, while installer failures are reported.
Node's `fetch` uses `HTTP(S)_PROXY` when opted in with `NODE_USE_ENV_PROXY=1`
(Node 22.21+ or 24.0+) or `--use-env-proxy` (Node 22.21+ or 24.5+).
For CI, set `AGENT_SERVE_API_KEY` and `AGENT_SERVE_WORKSPACE`; no saved login or config file is
required.
## Choosing a workspace [#choosing-a-workspace]
Workspace-scoped commands need a workspace:
```bash
agentserve tables list --workspace 9b4c7e02-1d58-4f36-a0c9-6e2b85df413a
agentserve configure --set-workspace 2f6d0b1c-8a34-4d92-b7e5-31c8a0f45d67
export AGENT_SERVE_WORKSPACE=2f6d0b1c-8a34-4d92-b7e5-31c8a0f45d67
```
For a reusable selection, create a workspace profile backed by the current
stored login:
```bash
agentserve workspaces list
agentserve profile add acme --workspace 7e2d9c14-6b83-4a55-8f01-c4d3e9a76b28
agentserve --profile acme tables list
```
When `--workspace` is omitted in a terminal, `profile add` presents an
interactive picker, capped at 1,000 entries. It refuses environment-only keys
and endpoint overrides because those values would disappear in another shell.
`agentserve billing status`, `agentserve billing logs`, and `agentserve audit-logs list` accept
`--all-workspaces` to drop the filter instead. It cannot be combined with
`--workspace`.
## Repairing a bad setting [#repairing-a-bad-setting]
An invalid `output` value fails with the list of accepted formats. A
higher-priority source still wins, so you can repair a profile without editing
the file:
```bash
agentserve --output table configure --set-output json
```
---
# Connector Types (/cli/connector-types)
Every command below also accepts the [global options](/cli/commands#global-options).
## List connector types [#list-connector-types]
```bash
agentserve connector-types list [options]
```
**Options**
| Option | Required | Description |
| ------------------ | -------- | ------------------------------------------------------------ |
| `--search ` | No | Case-insensitive substring match against the connector name. |
---
# Credentials (/cli/credentials)
`agentserve credentials` is also spelled `agentserve credential`.
Every command below also accepts the [global options](/cli/commands#global-options).
## Disconnect credential [#disconnect-credential]
```bash
agentserve credentials delete [options]
```
Disconnect Credential (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ------------------------- |
| `credentialId` | Yes | Credential to disconnect. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## List credential members [#list-credential-members]
```bash
agentserve credentials members list [options]
```
List Credential Members (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | -------------------------------------------- |
| `credentialId` | Yes | Credential whose sharing grants are managed. |
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `email`, `name`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
## Remove credential member [#remove-credential-member]
```bash
agentserve credentials members remove [options]
```
Remove Credential Member (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | -------------------------------------------- |
| `credentialId` | Yes | Credential whose sharing grants are managed. |
| `userId` | Yes | User whose explicit grant will be revoked. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Upsert credential member [#upsert-credential-member]
```bash
agentserve credentials members upsert [options]
```
Upsert Credential Member (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | -------------------------------------------- |
| `credentialId` | Yes | Credential whose sharing grants are managed. |
**Options**
| Option | Required | Description |
| ---------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `--user ` | Yes | Existing workspace member to grant or change access for. |
| `--role ` | Yes | Credential role to grant; workspace administrators cannot be demoted. Accepted values: `admin`, `member`. |
## List credential providers [#list-credential-providers]
```bash
agentserve credentials providers list [options]
```
**Options**
| Option | Required | Description |
| ------------------ | -------- | ---------------------------------------------------------------------- |
| `--search ` | No | Case-insensitive substring match against the credential provider name. |
## List credentials [#list-credentials]
```bash
agentserve credentials list [options]
```
**Options**
| Option | Required | Description |
| ----------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `--type ` | No | Restrict results to this credential type. Accepted values: `oauth`, `service_account`. |
| `--provider-id ` | No | Restrict results to credentials for this integration provider. |
| `--search ` | No | Case-insensitive substring match against the credential display name. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `displayName`, `createdAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
## Update credential [#update-credential]
```bash
agentserve credentials update [options]
```
Update Credential (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | --------------------- |
| `credentialId` | Yes | Credential to update. |
**Options**
| Option | Required | Description |
| ---------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `--display-name ` | No | New name shown for the credential in Agent Serve. |
| `--description ` | No | New credential description. Send null to clear the stored one. (--description null sends the word, not JSON null). |
| `--service-account-json ` | No | Write-only provider service-account JSON configuration, including Oracle Database connection fields. |
| `--api-token ` | No | Write-only provider API token. |
| `--domain ` | No | Provider account domain. |
| `--atlassian-product ` | No | Atlassian product to verify; defaults to Jira on create and preserves the saved product on reconnect. Accepted values: `jira`, `confluence`. |
| `--signing-secret ` | No | Write-only webhook signing secret. |
| `--bot-token ` | No | Write-only bot token. |
| `--client-id ` | No | OAuth client identifier. |
| `--client-secret ` | No | Write-only OAuth client secret. |
| `--certificate-id ` | No | Provider certificate mapping identifier. |
| `--org-id ` | No | Provider organization ID. |
| `--data-center ` | No | Provider data center. |
| `--auth-method ` | No | Provider authentication method. |
| `--private-key ` | No | Write-only PEM private key. |
| `--username ` | No | Provider run-as username. |
| `--tenancy-ocid ` | No | OCI tenancy OCID. |
| `--user-ocid ` | No | OCI user OCID. |
| `--fingerprint ` | No | OCI API-key fingerprint. |
| `--private-key-passphrase ` | No | Write-only OCI private-key passphrase. |
| `--region ` | No | OCI home region. |
| `--name ` | No | Alias for --display-name. |
## Create a service-account credential using its discovered provider schema [#create-a-service-account-credential-using-its-discovered-provider-schema]
```bash
agentserve credentials create [options]
```
Create a service-account credential using its discovered provider schema (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ------------ | -------- | --------------------------------------------------- |
| `providerId` | Yes | Service-account provider to create a credential for |
**Options**
| Option | Required | Description |
| ----------------------------- | -------- | --------------------------------------------------------------------- |
| `--name ` | Yes | Name shown for the credential in Agent Serve. |
| `--credentials ` | Yes | Provider credentials as JSON (or @path / @- to read a file or stdin). |
| `--description ` | No | Optional credential description. |
| `--id ` | No | Client-generated credential ID when provider discovery requires it. |
## Create a short-lived link for connecting an OAuth provider [#create-a-short-lived-link-for-connecting-an-oauth-provider]
```bash
agentserve credentials connect [options]
```
Create a short-lived link for connecting an OAuth provider (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ------------ | -------- | ------------------------- |
| `providerId` | Yes | OAuth provider to connect |
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ------------------------------------------------- |
| `--name ` | Yes | Name shown for the new credential in Agent Serve. |
## Create a short-lived link for reconnecting an OAuth credential [#create-a-short-lived-link-for-reconnecting-an-oauth-credential]
```bash
agentserve credentials reconnect
```
Create a short-lived link for reconnecting an OAuth credential (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ----------------------------------------- |
| `credentialId` | Yes | Existing OAuth credential to re-authorize |
---
# Custom Tools (/cli/custom-tools)
`agentserve custom-tools` is also spelled `agentserve custom-tool`.
Every command below also accepts the [global options](/cli/commands#global-options).
## Create custom tool [#create-custom-tool]
```bash
agentserve custom-tools create [options]
```
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--title ` | Yes | Display title, unique within the workspace. |
| `--schema ` | Yes | OpenAI function schema: \{"type":"function","function":\{"name":"...","parameters":\{"type":"object","properties":\{}}}} (JSON, or @path / @- to read a file or stdin). |
| `--code ` | Yes | Tool implementation executed in the sandboxed function runtime. |
## Delete custom tool [#delete-custom-tool]
```bash
agentserve custom-tools delete [options]
```
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ------------------------------ |
| `customToolId` | Yes | Unique custom tool identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Get custom tool [#get-custom-tool]
```bash
agentserve custom-tools get
```
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ------------------------------ |
| `customToolId` | Yes | Unique custom tool identifier. |
## List custom tools [#list-custom-tools]
```bash
agentserve custom-tools list [options]
```
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------- |
| `--search ` | No | Case-insensitive substring match against the tool title. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `title`, `createdAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
## Update custom tool [#update-custom-tool]
```bash
agentserve custom-tools update [options]
```
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ------------------------------ |
| `customToolId` | Yes | Unique custom tool identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--title ` | No | New display title for the tool. |
| `--schema ` | No | OpenAI function schema: \{"type":"function","function":\{"name":"...","parameters":\{"type":"object","properties":\{}}}} (JSON, or @path / @- to read a file or stdin). |
| `--code ` | No | Replacement tool implementation. |
---
# Files (/cli/files)
`agentserve files` is also spelled `agentserve file`.
Every command below also accepts the [global options](/cli/commands#global-options).
## Delete several files at once [#delete-several-files-at-once]
```bash
agentserve files batch-delete [options]
```
**Options**
| Option | Required | Description |
| ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--file-ids ` | Yes | File identifiers to update. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `-y, --yes` | Yes | Confirm this operation. |
## POST /api/v2/files/copy [#post-apiv2filescopy]
```bash
agentserve files copy [options]
```
**Options**
| Option | Required | Description |
| ----------------------------- | -------- | --------------------------------------------------------------------------------------------- |
| `--source ` | Yes | Source owner and selected fileIds or folderIds (JSON, or @path / @- to read a file or stdin). |
| `--destination ` | Yes | Destination owner and optional folderId (JSON, or @path / @- to read a file or stdin). |
## Create file [#create-file]
```bash
agentserve files create [options]
```
**Options**
| Option | Required | Description |
| ------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--name ` | Yes | File name, including its extension. Path separators and dot segments are rejected. |
| `--content-type ` | No | MIME type. When omitted, it is inferred from the file extension. |
| `--folder ` | No | Folder path as shown in the app; the leading / is optional. |
| `--content ` | No | Initial file content. Omit or send an empty string for a zero-byte file. The 70,000,000-character bound guards the JSON envelope; the decoded bytes must be at most 50 MiB, and a longer base64 payload is rejected with `413`. Use an upload session for anything larger. |
| `--encoding ` | No | Encoding of the content field. Accepted values: `utf-8`, `base64`. |
## Create a file folder at a path [#create-a-file-folder-at-a-path]
```bash
agentserve files folders create
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------------------------------------------------- |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
## Delete folder [#delete-folder]
```bash
agentserve files folders delete [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------------------------------------------------- |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
**Options**
| Option | Required | Description |
| ------------- | -------- | -------------------------------------- |
| `--recursive` | No | Delete the folder and its descendants. |
| `-y, --yes` | Yes | Confirm this operation. |
## List folders [#list-folders]
```bash
agentserve files folders list [options]
```
Also available as `agentserve files folders ls`.
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--parent ` | No | Direct parent folder path. |
| `--search ` | No | Case-insensitive substring match against the folder name. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--scope ` | No | Which lifecycle set to list: `active` (default) returns live folders only; `archived` returns folders a recursive delete soft-deleted, which is how a caller finds a path to hand to the folder restore. Authorization is identical for both. Accepted values: `active`, `archived`. |
| `--recursive ` | No | Whether parentPath includes every descendant instead of direct children only. Accepted values: `true`, `1`, `yes`, `on`, `y`, `enabled`, `false`, `0`, `no`, `off`, `n`, `disabled`. |
| `--depth ` | No | Deepest level below parentPath to include when recursive is true. |
## Rename or move a file folder [#rename-or-move-a-file-folder]
```bash
agentserve files folders move
```
Also available as `agentserve files folders mv`.
**Arguments**
| Argument | Required | Description |
| ------------- | -------- | ---------------------------------------------------------- |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
| `destination` | Yes | Folder path as shown in the app; the leading / is optional |
## Restore an archived file folder [#restore-an-archived-file-folder]
```bash
agentserve files folders restore
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------------------------------------------------- |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
## Delete file [#delete-file]
```bash
agentserve files delete [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Permanently delete a previous version of a file [#permanently-delete-a-previous-version-of-a-file]
```bash
agentserve files versions delete [options]
```
**Arguments**
| Argument | Required | Description |
| --------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
| `version` | Yes | Version number. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Show the metadata of one version of a file [#show-the-metadata-of-one-version-of-a-file]
```bash
agentserve files versions describe
```
**Arguments**
| Argument | Required | Description |
| --------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
| `version` | Yes | Version number. |
## List the recorded versions of a file [#list-the-recorded-versions-of-a-file]
```bash
agentserve files versions list [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| ---------------------- | -------- | -------------------------------------------------------------- |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `version`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
## Read the text content of one version of a file [#read-the-text-content-of-one-version-of-a-file]
```bash
agentserve files versions read [options]
```
**Arguments**
| Argument | Required | Description |
| --------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
| `version` | Yes | Version number. |
**Options**
| Option | Required | Description |
| --------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `--max-bytes ` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
| `--offset ` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
| `--limit ` | No | How many lines to return from `offset`. Absent reads to the end. |
## Make a previous version of a file current again [#make-a-previous-version-of-a-file-current-again]
```bash
agentserve files versions revert [options]
```
**Arguments**
| Argument | Required | Description |
| --------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
| `version` | Yes | Version number. |
**Options**
| Option | Required | Description |
| ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--expected-current-version ` | No | Revert only while this is still the current version; otherwise the request fails with `409`. Omit to revert whatever is current. Collaborative edits and repeated workflow writes that fold into the current version keep its number, so prefer `expectedRevision` to guard content. |
| `--expected-revision ` | No | Revert only while the file still holds the content this revision names, as returned by Get File Metadata or an earlier write; otherwise the request fails with `409`. Unlike a version number, it also catches edits that folded into the current version. |
## Download the content of one version of a file [#download-the-content-of-one-version-of-a-file]
```bash
agentserve files versions download [options]
```
**Arguments**
| Argument | Required | Description |
| --------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
| `version` | Yes | Version number. |
**Options**
| Option | Required | Description |
| -------------------------- | -------- | --------------------------------------------- |
| `-o, --output-file ` | No | Write content to a file instead of stdout. |
| `--force` | No | Overwrite --output-file if it already exists. |
## Apply one exact or anchor-based edit to a text file [#apply-one-exact-or-anchor-based-edit-to-a-text-file]
```bash
agentserve files edit [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--edit ` | Yes | One edit object: \{"mode":"search\_replace","search":"old","content":"new","replaceAll":false}, \{"mode":"replace\_between","beforeAnchor":"start line","afterAnchor":"end line","content":"new"}, \{"mode":"insert\_after","anchor":"line","content":"new"}, or \{"mode":"delete\_between","startAnchor":"first line deleted","endAnchor":"ending line kept"}. Anchored modes also accept occurrence starting at 1 (JSON, or @path / @- to read a file or stdin). |
| `--expected-revision ` | No | Revision from Get File Metadata or an earlier write; the request is refused with `409` when the content moved on. |
## Show file metadata and sharing status [#show-file-metadata-and-sharing-status]
```bash
agentserve files describe [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--scope ` | No | Which lifecycle set to read from: `active` (default) resolves live files only and returns `404` for a file a delete soft-deleted; `archived` also resolves soft-deleted files, so metadata stays readable before the file is restored. Authorization is identical for both. Accepted values: `active`, `archived`. |
## Show a file’s share settings [#show-a-files-share-settings]
```bash
agentserve files share get
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
## Enable or disable sharing for a file [#enable-or-disable-sharing-for-a-file]
```bash
agentserve files share set [options]
```
Enable or disable sharing for a file (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--is-active ` | Yes | Whether the share should resolve. Disabling preserves the token and the whole access configuration, so re-enabling restores the share as it was; enabling rewrites the credentials the resulting mode does not use. Accepted values: `true`, `false`. |
| `--auth-type ` | No | How access to the share is gated. The stored mode is kept when omitted. Enabling `public` clears the stored password and empties `allowedEmails`; `password` empties `allowedEmails`; `email` and `sso` clear the stored password. Accepted values: `public`, `password`, `email`, `sso`. |
| `--password ` | No | Password for a password-gated share. Kept when omitted; enabling `password` with neither a supplied nor a stored password is a 400. |
| `--allowed-emails ` | No | Allowed addresses or `@domain` patterns for email and SSO shares. Kept when omitted; enabling `email` or `sso` with an empty resulting list is a 400. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
## List files [#list-files]
```bash
agentserve files list [options]
```
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--folder ` | No | Folder path as shown in the app; the leading / is optional. |
| `--recursive` | No | Include subfolders in the folder filter. Defaults to true when searching and false otherwise. Ignored without a folder filter. |
| `--no-recursive` | No | Send --recursive as false. |
| `--scope ` | No | Which lifecycle set to list: `active` (default) for live files, `archived` for files a delete soft-deleted. `folderPath` resolves against active folders only, so pairing it with `scope=archived` returns an empty page when the containing folder was archived too. Accepted values: `active`, `archived`. |
| `--search ` | No | Case-insensitive substring match against the file name. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `size`, `uploadedAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
## Move files into another folder [#move-files-into-another-folder]
```bash
agentserve files move [options]
```
Also available as `agentserve files mv`.
**Options**
| Option | Required | Description |
| ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--file-ids ` | Yes | File identifiers to update. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--to ` | No | Destination folder path; omit for root. |
## Read a file’s text content as JSON or YAML [#read-a-files-text-content-as-json-or-yaml]
```bash
agentserve files read [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| --------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `--max-bytes ` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
| `--offset ` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
| `--limit ` | No | How many lines to return from `offset`. Absent reads to the end. |
## Rename a file [#rename-a-file]
```bash
agentserve files rename [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| ---------------- | -------- | --------------------------------------- |
| `--name ` | Yes | New file name, including its extension. |
## Restore an archived file [#restore-an-archived-file]
```bash
agentserve files restore
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
## Search file content [#search-file-content]
```bash
agentserve files search [options]
```
**Options**
| Option | Required | Description |
| ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--query ` | Yes | Regular expression, or exact text when `mode` is `exact`. |
| `--mode ` | No | How `query` is read. Accepted values: `exact`, `regex`. |
| `--max-results ` | No | Maximum matching lines to return. |
| `--folder ` | No | Folders to search, by path as shown in the app; omit to search the whole workspace (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--include-subfolders` | No | Whether each folder scope includes nested folders; on by default. |
| `--no-include-subfolders` | No | Send --include-subfolders as false. |
## Unzip an archive into a new folder beside it [#unzip-an-archive-into-a-new-folder-beside-it]
```bash
agentserve files unzip [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Replace a file’s contents [#replace-a-files-contents]
```bash
agentserve files set-content [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------- |
| `fileId` | Yes | File identifier. |
**Options**
| Option | Required | Description |
| ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--content ` | Yes | Complete replacement content for the file. The 70,000,000-character bound guards the JSON envelope; the decoded bytes must be at most 50 MiB, and a longer base64 payload is rejected with `413`. |
| `--encoding ` | No | Content encoding. Accepted values: `utf-8`, `base64`. |
| `--expected-revision ` | No | Revision from Get File Metadata or an earlier write; the request is refused with `409` when the content moved on. |
## Upload a file to the workspace [#upload-a-file-to-the-workspace]
```bash
agentserve files upload [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | -------------------- |
| `path` | Yes | Local file to upload |
**Options**
| Option | Required | Description |
| ----------------- | -------- | ------------------------------------------------------------- |
| `--folder ` | No | Folder path as shown in the app; defaults to the root folder. |
| `--name ` | No | Store it under a different name. |
## Download a file’s content to stdout or a local file [#download-a-files-content-to-stdout-or-a-local-file]
```bash
agentserve files get [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | -------------------------- |
| `fileId` | Yes | File whose content to read |
**Options**
| Option | Required | Description |
| -------------------------- | -------- | --------------------------------------------- |
| `-o, --output-file ` | No | Write content to a file instead of stdout. |
| `--force` | No | Overwrite --output-file if it already exists. |
## List file resources and child folders together [#list-file-resources-and-child-folders-together]
```bash
agentserve files ls [path] [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ------------------------------------------------ |
| `path` | No | Folder path to list; defaults to the root folder |
**Options**
| Option | Required | Description |
| ----------------- | -------- | --------------------------------------------------------------------- |
| `--search ` | No | Filter folders and resources by name. |
| `--limit ` | No | Maximum combined items to return (0 for everything). Defaults to `0`. |
## Create a file directory at a path [#create-a-file-directory-at-a-path]
```bash
agentserve files mkdir
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ------------------------------------------------ |
| `path` | Yes | Folder path to create; the leading / is optional |
---
# Agent Serve CLI (/cli)
`agentserve` is the command line for Agent Serve. Sign in once, then run workflows, query tables,
move files, search knowledge bases, and read run logs from the terminal. Results
are JSON by default, so they pipe into `jq`, cron jobs, CI pipelines, and coding
agents; use `--output table` to read them as tables. Don't know a command? Run
`agentserve cli search ""`. See [Output formats](/cli/output) for commands that emit raw content
or local configuration.
## Install [#install]
```bash
npm install -g agentserve
```
```bash
pnpm add -g agentserve
```
```bash
bun add -g agentserve
```
Requires Node.js 20 or newer. Verify with `agentserve --version`.
The CLI shows optional update notices on eligible interactive invocations. Run
`agentserve update` when you want to install the update. See
[Updates](/cli/configuration#updates) for installation requirements and how to
disable notices.
To run it without installing, use `npx agentserve `.
Using Agent Serve as a library instead? See the [TypeScript](/api-reference/typescript)
and [Python](/api-reference/python) SDKs, or the
[HTTP API](/api-reference/getting-started).
## Your first command [#your-first-command]
### Sign in [#sign-in]
```bash
agentserve login
```
The CLI opens your browser for approval. Local sign-in prefers OAuth; remote
terminals use API-key pairing. After OAuth sign-in, choose a default workspace
with `agentserve configure --set-workspace `. For SSH and containers, use
`agentserve login --method api-key --no-browser` and approve the pairing code.
See [Authentication](/cli/authentication) for CI keys, multiple accounts, and
self-hosted deployments.
### Check what you are pointed at [#check-what-you-are-pointed-at]
```bash
agentserve whoami
```
This prints the resolved endpoint, workspace, and output format — and **where
each one came from**. It is the fastest way to explain a surprising result.
### List your workflows [#list-your-workflows]
```bash
agentserve workflows list
```
```
ID NAME FOLDER DEPLOYED RUNS LAST RUN
3a9e21d8-5f47-4c0b-b2ea-91d7c6034ef8 Refund triage /Support yes 412 2026-08-15 14:02:11
b8c0d247-9e13-4a86-97f5-2ad4e1638c09 Weekly digest /Reporting no 18 2026-08-11 09:00:04
```
### Run one [#run-one]
```bash
agentserve workflows run 3a9e21d8-5f47-4c0b-b2ea-91d7c6034ef8 --input '{"ticketId":"T-4821"}'
```
This command runs the active deployment. Deploy from the editor or with
`agentserve workflows deploy `. To run the current saved workflow state instead,
use `agentserve workflows run --manual`.
## How commands are shaped [#how-commands-are-shaped]
Every command reads the same way:
```bash
agentserve [sub-resource] [arguments] [options]
```
```bash
agentserve workflows list
agentserve tables rows query tbl_9f3c1a05d4b7426e8c2f0917ab35de64 --limit 50
agentserve knowledge documents upload 4c1b7f60-2d55-4a3e-9c18-70b6ea2f9d31 ./handbook.pdf
```
Some resource groups also accept a singular alias: `agentserve workflow get` and
`agentserve workflows get` are the same command. `knowledge` also answers to `kb`.
Each group’s reference lists its supported aliases.
Every command accepts `--help`, at any depth:
```bash
agentserve --help
agentserve tables --help
agentserve tables rows query --help
```
## What you can do [#what-you-can-do]
| Group | What it covers |
| --------------------------------------------------- | ------------------------------------------------------------------ |
| [`workflows`](/cli/workflows) | Run, deploy, roll back, import, export, and organize workflows |
| [`logs`](/cli/logs) | Read run diagnostics, including the full trace tree |
| [`tables`](/cli/tables) | Query, insert, update, and import rows; manage columns and views |
| [`files`](/cli/files) | Upload, download, share, and organize workspace files |
| [`knowledge`](/cli/knowledge) | Search knowledge bases and manage their documents and tags |
| [`skills`](/cli/skills) | Manage agent skills |
| [`sandboxes`](/cli/sandboxes) | Manage sandbox dependencies, managed CLIs, and system packages |
| [`mcp-servers`](/cli/mcp-servers) | Manage MCP server connections and their tools |
| [`custom-tools`](/cli/custom-tools) | Manage custom tool definitions |
| [`credentials`](/cli/credentials) | Connect, reconnect, and disconnect integration credentials |
| [`secrets`](/cli/secrets) | Set and remove workspace secrets |
| [`billing`](/cli/billing) | Check plan status and credit usage |
| [`audit-logs`](/cli/audit-logs) | Read organization audit logs |
| [`workspaces`](/cli/workspaces) | Inspect the active workspace and its members |
| [`blocks`](/cli/blocks) | Browse the block catalog and read one block's configuration fields |
| [`tools`](/cli/tools) | Browse and execute built-in tools |
| [`connector-types`](/cli/connector-types) | Browse knowledge-base connector types and their config fields |
| [`chat-deployments`](/cli/chat-deployments) | List the hosted chats a workspace serves |
| [`workflow-mcp-servers`](/cli/workflow-mcp-servers) | Publish workflows as MCP tools for outside agents |
| [`meta`](/cli/meta) | Check what this API supports and which limits apply |
The [command overview](/cli/commands) has the global options and the commands
that take no resource; the [complete reference](/cli/reference) documents every
subcommand, argument, and flag on one page. Both are generated from the CLI
itself.
## Where to go next [#where-to-go-next]
* [Authentication](/cli/authentication) — signing in, API keys for CI, and multiple accounts
* [Configuration](/cli/configuration) — profiles, config files, environment variables, and precedence
* [Output formats](/cli/output) — `table`, `json`, `yaml`, and `text`, and when to use each
* [Scripting](/cli/scripting) — piping, file inputs, exit codes, and automation recipes
* [Troubleshooting](/cli/troubleshooting) — what each error means, and how to resolve it
---
# Knowledge (/cli/knowledge)
`agentserve knowledge` is also spelled `agentserve kb`.
Every command below also accepts the [global options](/cli/commands#global-options).
## Index files the workspace already stores [#index-files-the-workspace-already-stores]
```bash
agentserve knowledge from-workspace-files create [options]
```
Index files the workspace already stores (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `--file ` | Yes | Workspace file ID or key (repeatable) (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
## Declare the tag definitions a knowledge base needs [#declare-the-tag-definitions-a-knowledge-base-needs]
```bash
agentserve knowledge tags save [options]
```
Declare the tag definitions a knowledge base needs (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `--definitions ` | Yes | Tag definitions: \[\{"tagSlot":"tag1","displayName":"category","fieldType":"text"}] (JSON, or @path / @- to read a file or stdin). |
## Create tag [#create-tag]
```bash
agentserve knowledge tags create [options]
```
Create Tag (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--display-name ` | Yes | Name tag filters and document reads use for this tag. |
| `--field-type ` | No | Value type stored in the slot; it decides which slots are usable and which filter operators apply. Defaults to text, so a number, date, or boolean slot must name its type here. Slot capacity per type: text 7, number 5, date 2, boolean 3. Accepted values: `text`, `number`, `date`, `boolean`. |
| `--tag-slot ` | No | Slot to store the tag in. Omit to take the next free slot for the field type; a slot that does not belong to the field type, or one already in use, is rejected. Accepted values: `tag1`, `tag2`, `tag3`, `tag4`, `tag5`, `tag6`, `tag7`, `number1`, `number2`, `number3`, `number4`, `number5`, `date1`, `date2`, `boolean1`, `boolean2`, `boolean3`. |
## Delete tag [#delete-tag]
```bash
agentserve knowledge tags delete [options]
```
Delete Tag (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `tagId` | Yes | Unique tag definition identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Remove tag definitions no document still uses [#remove-tag-definitions-no-document-still-uses]
```bash
agentserve knowledge tags cleanup [options]
```
Remove tag definitions no document still uses (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--unused` | No | Whether to remove only the tag definitions no document in the knowledge base still carries a value for. Defaults to true. Pass --no-unused to delete every definition on the knowledge base, which also clears its slot on every document and chunk and is not recoverable. |
| `--no-unused` | No | Send --unused as false. |
| `-y, --yes` | Yes | Confirm this operation. |
## Show which tag slot a create would take for a field type [#show-which-tag-slot-a-create-would-take-for-a-field-type]
```bash
agentserve knowledge tags next-slot [options]
```
Show which tag slot a create would take for a field type (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--field-type ` | Yes | Value type stored in the slot; it decides which slots are usable and which filter operators apply. Slot capacity per type: text 7, number 5, date 2, boolean 3. Accepted values: `text`, `number`, `date`, `boolean`. |
## List tags [#list-tags]
```bash
agentserve knowledge tags list
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
## Show how many documents and chunks carry each tag [#show-how-many-documents-and-chunks-carry-each-tag]
```bash
agentserve knowledge tags usage
```
Show how many documents and chunks carry each tag (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
## Update tag [#update-tag]
```bash
agentserve knowledge tags update [options]
```
Update Tag (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `tagId` | Yes | Unique tag definition identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | --------------------------------------------------------------------------------- |
| `--display-name ` | No | New tag display name. |
| `--field-type ` | No | New value type for the tag. Accepted values: `text`, `number`, `date`, `boolean`. |
## Enable, disable, or delete many chunks at once [#enable-disable-or-delete-many-chunks-at-once]
```bash
agentserve knowledge chunks batch-update [options]
```
Enable, disable, or delete many chunks at once (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
**Options**
| Option | Required | Description |
| --------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--operation ` | Yes | What to do with the selected chunks. Accepted values: `enable`, `disable`, `delete`. |
| `--chunk ` | Yes | Chunks to operate on, by identifier. An id naming no chunk in the document is reported in errors and does not fail the request. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `-y, --yes` | Yes | Confirm this operation. |
## Create chunk [#create-chunk]
```bash
agentserve knowledge chunks create [options]
```
Create Chunk (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
**Options**
| Option | Required | Description |
| ------------------- | -------- | ------------------------------------------------------------------------------- |
| `--content ` | Yes | Text to embed. It is embedded on write, so the chunk is searchable immediately. |
| `--enabled` | No | Whether the new chunk participates in search. |
| `--no-enabled` | No | Send --enabled as false. |
## Delete chunk [#delete-chunk]
```bash
agentserve knowledge chunks delete [options]
```
Delete Chunk (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
| `chunkId` | Yes | Unique chunk identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Get chunk [#get-chunk]
```bash
agentserve knowledge chunks get
```
Get Chunk (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
| `chunkId` | Yes | Unique chunk identifier. |
## List chunks [#list-chunks]
```bash
agentserve knowledge chunks list [options]
```
List Chunks (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `--search ` | No | Case-insensitive substring match against chunk content. |
| `--enabled ` | No | Restrict to enabled or disabled chunks. `all` returns both. Accepted values: `true`, `false`, `all`. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `chunkIndex`, `tokenCount`, `enabled`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
## Update chunk [#update-chunk]
```bash
agentserve knowledge chunks update [options]
```
Update Chunk (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
| `chunkId` | Yes | Unique chunk identifier. |
**Options**
| Option | Required | Description |
| ------------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `--content ` | No | Replacement text. Changing it re-embeds the chunk and re-derives its token and character counts. |
| `--enabled` | No | Whether the chunk participates in search. Disabling keeps it indexed. |
| `--no-enabled` | No | Send --enabled as false. |
## Enable or disable every matching document [#enable-or-disable-every-matching-document]
```bash
agentserve knowledge documents batch-update [options]
```
Enable or disable every matching document (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `--operation ` | Yes | Whether the selected documents become enabled or disabled for search. Accepted values: `enable`, `disable`. |
| `--document ` | No | Documents to update, by identifier. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--select-all` | No | Apply to every document in the knowledge base. |
| `--enabled-filter ` | No | With `selectAll`, restrict the update to documents in this state. Accepted values: `all`, `enabled`, `disabled`. |
## Delete a document from a knowledge base [#delete-a-document-from-a-knowledge-base]
```bash
agentserve knowledge documents delete [options]
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Get document [#get-document]
```bash
agentserve knowledge documents get
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
## List documents [#list-documents]
```bash
agentserve knowledge documents list [options]
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| -------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--search ` | No | Case-insensitive substring match against the document filename. |
| `--enabled-filter ` | No | Filter by whether documents are enabled for search. Accepted values: `all`, `enabled`, `disabled`. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `filename` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `filename`, `fileSize`, `tokenCount`, `chunkCount`, `uploadedAt`, `processingStatus`, `enabled`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
| `--tag-filters ` | No | A JSON-encoded array of at most 10 tag filters, using the same display-name shape as knowledge search: `[{"tagName":"category","operator":"eq","value":"billing"}]`. Every filter must hold, including two that name the same tag. A name that is not defined in this knowledge base is rejected, never ignored. |
## Update document [#update-document]
```bash
agentserve knowledge documents update [options]
```
Update Document (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
**Options**
| Option | Required | Description |
| -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--filename ` | No | New filename for the document. |
| `--enabled` | No | Whether the document participates in search. Disabling keeps it indexed. |
| `--no-enabled` | No | Send --enabled as false. |
| `--tag1 ` | No | New value for tag slot 1. |
| `--tag2 ` | No | New value for tag slot 2. |
| `--tag3 ` | No | New value for tag slot 3. |
| `--tag4 ` | No | New value for tag slot 4. |
| `--tag5 ` | No | New value for tag slot 5. |
| `--tag6 ` | No | New value for tag slot 6. |
| `--tag7 ` | No | New value for tag slot 7. |
| `--number1 ` | No | New value for number tag slot 1. |
| `--number2 ` | No | New value for number tag slot 2. |
| `--number3 ` | No | New value for number tag slot 3. |
| `--number4 ` | No | New value for number tag slot 4. |
| `--number5 ` | No | New value for number tag slot 5. |
| `--date1 ` | No | New value for date tag slot 1, formatted YYYY-MM-DD. |
| `--date2 ` | No | New value for date tag slot 2, formatted YYYY-MM-DD. |
| `--boolean1` | No | New value for boolean tag slot 1. |
| `--no-boolean1` | No | Send --boolean1 as false. |
| `--boolean2` | No | New value for boolean tag slot 2. |
| `--no-boolean2` | No | Send --boolean2 as false. |
| `--boolean3` | No | New value for boolean tag slot 3. |
| `--no-boolean3` | No | Send --boolean3 as false. |
| `--retry-processing` | No | Requeue a failed or stuck document for processing. Send it alone — no other field may accompany it — and it answers with a queue acknowledgement rather than the document. |
## Upload a document to a knowledge base [#upload-a-document-to-a-knowledge-base]
```bash
agentserve knowledge documents upload [options]
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ----------------------------- |
| `knowledgeBaseId` | Yes | Knowledge base to upload into |
| `path` | Yes | Local file to upload |
**Options**
| Option | Required | Description |
| ------------------ | -------- | ------------------------------------------------------------------------------------------ |
| `--name ` | No | Store it under a different name. |
| `--tag ` | No | Document tags, in tag1 through tag7 order. |
| `--recipe ` | No | Document processing recipe. Accepted values: `default`, `plain`, `markdown`, `code`. |
| `--lang ` | No | Document language tag: hyphen-separated letter and digit subtags, for example en or en-US. |
## Create knowledge base [#create-knowledge-base]
```bash
agentserve knowledge create [options]
```
**Options**
| Option | Required | Description |
| --------------------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `--name ` | Yes | Human-readable knowledge base name. |
| `--description ` | No | Optional knowledge base description. |
| `--chunking-config ` | No | Chunking configuration; defaults are applied when omitted. (JSON, or @path / @- to read a file or stdin). |
| `--folder ` | No | Folder path as shown in the app; the leading / is optional. |
## Create knowledge connector [#create-knowledge-connector]
```bash
agentserve knowledge connectors create [options]
```
Create Knowledge Connector (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| --------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| `--connector-type ` | Yes | Registered connector type. |
| `--credential-id ` | No | OAuth credential identifier for connectors that require OAuth. |
| `--api-key ` | No | Write-only API key for connectors that use API-key authentication. |
| `--source-config ` | Yes | Connector-specific source selection and filtering configuration. (JSON, or @path / @- to read a file or stdin). |
| `--sync-interval-minutes ` | No | Scheduled synchronization interval in minutes; zero disables scheduling. |
## Delete knowledge connector [#delete-knowledge-connector]
```bash
agentserve knowledge connectors delete [options]
```
Delete Knowledge Connector (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------------- |
| `knowledgeBaseId` | Yes | Knowledge base that owns the connector. |
| `connectorId` | Yes | Connector selected for the operation. |
**Options**
| Option | Required | Description |
| ----------------------- | -------- | ------------------------------------------------------------- |
| `--delete-documents` | No | Also permanently delete documents produced by this connector. |
| `--no-delete-documents` | No | Send --delete-documents as false. |
| `-y, --yes` | Yes | Confirm this operation. |
## Get knowledge connector [#get-knowledge-connector]
```bash
agentserve knowledge connectors get
```
Get Knowledge Connector (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------------- |
| `knowledgeBaseId` | Yes | Knowledge base that owns the connector. |
| `connectorId` | Yes | Connector selected for the operation. |
## List knowledge connector documents [#list-knowledge-connector-documents]
```bash
agentserve knowledge connectors documents list [options]
```
List Knowledge Connector Documents (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------------- |
| `knowledgeBaseId` | Yes | Knowledge base that owns the connector. |
| `connectorId` | Yes | Connector selected for the operation. |
**Options**
| Option | Required | Description |
| ----------------------- | -------- | -------------------------------------------------------------- |
| `--include-excluded` | No | Include documents explicitly excluded by a user. |
| `--no-include-excluded` | No | Send --include-excluded as false. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
## Update knowledge connector documents [#update-knowledge-connector-documents]
```bash
agentserve knowledge connectors documents update [options]
```
Update Knowledge Connector Documents (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------------- |
| `knowledgeBaseId` | Yes | Knowledge base that owns the connector. |
| `connectorId` | Yes | Connector selected for the operation. |
**Options**
| Option | Required | Description |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `--operation ` | Yes | Whether to restore or exclude the selected documents. Accepted values: `restore`, `exclude`. |
| `--document ` | Yes | Connector document identifiers to update. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
## List knowledge connectors [#list-knowledge-connectors]
```bash
agentserve knowledge connectors list [options]
```
List Knowledge Connectors (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------ |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `connectorType`, `createdAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
## Queue a knowledge connector synchronization [#queue-a-knowledge-connector-synchronization]
```bash
agentserve knowledge connectors sync [options]
```
Queue a knowledge connector synchronization (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------------- |
| `knowledgeBaseId` | Yes | Knowledge base that owns the connector. |
| `connectorId` | Yes | Connector selected for the operation. |
**Options**
| Option | Required | Description |
| ---------------- | -------- | -------------------------------------------------------- |
| `--rehydrate` | No | Re-fetch and re-index every existing connector document. |
| `--no-rehydrate` | No | Send --rehydrate as false. |
## Update knowledge connector [#update-knowledge-connector]
```bash
agentserve knowledge connectors update [options]
```
Update Knowledge Connector (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------------- |
| `knowledgeBaseId` | Yes | Knowledge base that owns the connector. |
| `connectorId` | Yes | Connector selected for the operation. |
**Options**
| Option | Required | Description |
| --------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--source-config ` | No | Replacement source selection and filtering configuration. Updating a runnable connector queues synchronization; paused connectors remain paused. (JSON, or @path / @- to read a file or stdin). |
| `--sync-interval-minutes ` | No | New scheduled synchronization interval in minutes. |
| `--status ` | No | New connector state. Accepted values: `active`, `paused`. |
## Create a knowledge folder at a path [#create-a-knowledge-folder-at-a-path]
```bash
agentserve knowledge folders create
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------------------------------------------------- |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
## Delete folder [#delete-folder]
```bash
agentserve knowledge folders delete [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ---------------------------------------------------------- |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
**Options**
| Option | Required | Description |
| ------------- | -------- | -------------------------------------- |
| `--recursive` | No | Delete the folder and its descendants. |
| `-y, --yes` | Yes | Confirm this operation. |
## List knowledge folders [#list-knowledge-folders]
```bash
agentserve knowledge folders list [options]
```
Also available as `agentserve knowledge folders ls`.
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--parent ` | No | Direct parent folder path. |
| `--search ` | No | Case-insensitive substring match against the folder name. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
## Rename or move a knowledge folder [#rename-or-move-a-knowledge-folder]
```bash
agentserve knowledge folders move
```
Also available as `agentserve knowledge folders mv`.
**Arguments**
| Argument | Required | Description |
| ------------- | -------- | ---------------------------------------------------------- |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
| `destination` | Yes | Folder path as shown in the app; the leading / is optional |
## Delete knowledge base [#delete-knowledge-base]
```bash
agentserve knowledge delete [options]
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Get knowledge base [#get-knowledge-base]
```bash
agentserve knowledge get
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
## List knowledge bases [#list-knowledge-bases]
```bash
agentserve knowledge list [options]
```
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--scope ` | No | Lifecycle scope: active or archived knowledge bases. Use Restore Knowledge Base to recover archived entries. Folder paths resolve only active folders, so filtering by an archived folder returns no matches. Accepted values: `active`, `archived`. |
| `--folder ` | No | Folder path as shown in the app; the leading / is optional. |
| `--search ` | No | Case-insensitive substring match against the resource name. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
## Restore an archived knowledge base [#restore-an-archived-knowledge-base]
```bash
agentserve knowledge restore
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
## Search knowledge [#search-knowledge]
```bash
agentserve knowledge search [options]
```
**Options**
| Option | Required | Description |
| -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--kb ` | Yes | Knowledge base ID (repeatable) (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--query ` | No | Text to search for. |
| `--top-k ` | No | Maximum number of search results to return. Must be a whole number between 1 and 100. |
| `--tag-filters ` | No | Tag filters as \[\{"tagName":"...","operator":"...","value":"..."}] (JSON, or @path / @- to read a file or stdin). |
| `--search-mode ` | No | Search algorithm. Accepted values: `vector`, `hybrid`. |
| `--reranker-enabled` | No | Re-order retrieved chunks with a reranking model before truncating to `topK`. Ignored for a tag-only search, and billed per rerank call on top of the search. Reranking is best-effort — a provider failure falls back to vector ordering, so check `rerankerStatus` on the response. A model your organization's model policy does not allow (including its zero-data-retention requirement) is rejected with 403. |
| `--no-reranker-enabled` | No | Send --reranker-enabled as false. |
| `--reranker-model ` | No | Reranking model to use when `rerankerEnabled` is true: a Cohere id (`rerank-v4.0-pro`, `rerank-v4.0-fast`, `rerank-v3.5`) or an OpenRouter rerank model as `openrouter/<maker>/<model>`, e.g. `openrouter/voyageai/rerank-2.5`. Defaults to `rerank-v4.0-fast`. (JSON, or @path / @- to read a file or stdin). |
| `--reranker-input-count ` | No | How many candidate chunks to retrieve before reranking. Defaults to four times `topK`, capped at 100. A larger pool costs more retrieval work but gives the reranker more to choose from. |
## Update knowledge base [#update-knowledge-base]
```bash
agentserve knowledge update [options]
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | --------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
**Options**
| Option | Required | Description |
| --------------------------------- | -------- | ----------------------------------------------------------------------------------- |
| `--name ` | No | New knowledge base name. |
| `--description ` | No | New knowledge base description. |
| `--chunking-config ` | No | New document chunking configuration. (JSON, or @path / @- to read a file or stdin). |
| `--folder ` | No | Folder path as shown in the app; the leading / is optional. |
## Move a knowledge base to a folder [#move-a-knowledge-base-to-a-folder]
```bash
agentserve knowledge mv
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ---------------------------------------------------------- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `folder` | Yes | Folder path as shown in the app; the leading / is optional |
## Export a knowledge base as a .agentservekb.zip bundle [#export-a-knowledge-base-as-a-agentservekbzip-bundle]
```bash
agentserve knowledge export [options]
```
**Arguments**
| Argument | Required | Description |
| ----------------- | -------- | ------------------------ |
| `knowledgeBaseId` | Yes | Knowledge base to export |
**Options**
| Option | Required | Description |
| -------------------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `-o, --output-file ` | No | Write the bundle to this path instead of the name the server suggests; pass - to stream it to stdout. |
| `--force` | No | Overwrite --output-file if it already exists. |
| `--no-vectors` | No | Leave chunk vectors out of the bundle, so an import re-embeds every chunk. |
## List knowledge resources and child folders together [#list-knowledge-resources-and-child-folders-together]
```bash
agentserve knowledge ls [path] [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ------------------------------------------------ |
| `path` | No | Folder path to list; defaults to the root folder |
**Options**
| Option | Required | Description |
| ----------------- | -------- | --------------------------------------------------------------------- |
| `--search ` | No | Filter folders and resources by name. |
| `--limit ` | No | Maximum combined items to return (0 for everything). Defaults to `0`. |
## Create a knowledge directory at a path [#create-a-knowledge-directory-at-a-path]
```bash
agentserve knowledge mkdir
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ------------------------------------------------ |
| `path` | Yes | Folder path to create; the leading / is optional |
---
# Logs (/cli/logs)
`agentserve logs` is also spelled `agentserve log`.
Every command below also accepts the [global options](/cli/commands#global-options).
## Show run diagnostics [#show-run-diagnostics]
```bash
agentserve logs get [options]
```
**Arguments**
| Argument | Required | Description |
| -------- | -------- | ------------------------------- |
| `runId` | Yes | Unique workflow run identifier. |
**Options**
| Option | Required | Description |
| ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `--include-workflow-state` | No | Include the saved workflow snapshot (default: true). Set false to omit block configuration from a log read. Other run fields are unchanged. |
| `--no-include-workflow-state` | No | Send --include-workflow-state as false. |
| `--trace` | No | Show expanded trace spans with inputs, outputs, errors, timing, and cost. |
## Summarize run counts, failures and latency over a window [#summarize-run-counts-failures-and-latency-over-a-window]
```bash
agentserve logs stats [options]
```
**Options**
| Option | Required | Description |
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--workflow ` | No | Comma-separated workflow identifiers to include. At most 200 entries. An empty entry is rejected. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--folder ` | No | Folder path as shown in the app; the leading / is optional (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--trigger ` | No | Comma-separated trigger types to include. An empty entry is rejected. The vocabulary is open, so an unrecognized member selects no runs; the literal `all` disables this filter. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--level ` | No | Severity level to include. Accepted values: `info`, `error`. |
| `--start-date ` | No | Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--end-date ` | No | Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--segment-count ` | No | Number of time buckets, up to 500. Exactly this many are returned, each at least one minute wide. Short windows extend past the requested end and include empty trailing buckets. |
## List run logs, such as failed or errored runs, by workflow, trigger or time [#list-run-logs-such-as-failed-or-errored-runs-by-workflow-trigger-or-time]
```bash
agentserve logs list [options]
```
**Options**
| Option | Required | Description |
| --------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--workflow ` | No | Comma-separated workflow identifiers to include. An empty entry is rejected. At most 200 entries. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--trigger ` | No | Comma-separated, lowercase trigger types or webhook provider IDs. Matching is exact and case-sensitive; unknown values select no runs. An empty entry is rejected. The sentinel `all` disables this filter, even when listed with other values. At most 100 entries. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
| `--level ` | No | Severity level to include. Accepted values: `info`, `error`. |
| `--start-date ` | No | Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--end-date ` | No | Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--min-duration-ms ` | No | Minimum total execution duration in milliseconds. Whole milliseconds from 0 to 2147483647; the stored duration is a 32-bit integer, so a fractional or out-of-range bound is rejected. |
| `--max-duration-ms ` | No | Maximum total execution duration in milliseconds. Whole milliseconds from 0 to 2147483647; the stored duration is a 32-bit integer, so a fractional or out-of-range bound is rejected. |
| `--min-cost ` | No | Minimum execution cost in USD, from 0 to 1000000. A run is never charged a negative amount, so a negative bound is rejected rather than treated as a filter that matches every run. |
| `--max-cost ` | No | Maximum execution cost in USD, from 0 to 1000000. A run is never charged a negative amount, so a negative bound is rejected rather than treated as a filter that matches every run. |
| `--model ` | No | AI model used during execution. |
| `--details ` | No | Response detail level; full is requested by default to name each run’s workflow. Accepted values: `basic`, `full`. |
| `--include-trace-spans` | No | Include trace spans in JSON or YAML output (implies full detail). |
| `--include-final-output` | No | Include final output in JSON or YAML output (implies full detail). |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
| `--status ` | No | Comma-separated execution statuses to include, from `pending` \| `running` \| `paused` \| `redacting` \| `completed` \| `failed` \| `cancelled`. An empty entry is rejected. ANDed with `level`, which reports severity rather than lifecycle. |
| `--workflow-name ` | No | Case-insensitive substring match against the run's workflow name. Runs whose workflow has been deleted match nothing, because the name is no longer joinable. |
| `--include-job-runs` | No | Include Chat and Agent Serve agent jobs alongside workflow runs. Jobs use `kind: "job"` and have no workflow or cost ledger. Workflow, folder, model, or status filters exclude jobs. This option is valid only when sorting by `startedAt`. |
| `--no-include-job-runs` | No | Send --include-job-runs as false. |
| `--run-id ` | No | Exact run identifier to match. |
| `--sort-by ` | No | Field used to sort the result. `durationMs` and `cost` are null until a run settles; those runs sort before recorded values in ascending order and after them in descending order. Only `startedAt` can order Chat and Agent Serve agent job runs, so any other value is rejected when job runs are included. Accepted values: `startedAt`, `durationMs`, `cost`, `status`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--folder ` | No | Folder path as shown in the app; the leading / is optional (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
## Watch runs live as they arrive, printing each new run once [#watch-runs-live-as-they-arrive-printing-each-new-run-once]
```bash
agentserve logs follow [options]
```
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `--workflow ` | No | Only follow runs of this workflow (repeatable). |
| `--folder ` | No | Only follow runs of workflows in this folder (repeatable). |
| `--trigger ` | No | Only follow runs with this trigger type (repeatable). |
| `--level ` | No | Only follow runs at this severity. Accepted values: `info`, `error`. |
| `--details ` | No | Response detail level; full names each run’s workflow. Accepted values: `basic`, `full`. Defaults to `full`. |
| `-n, --lines ` | No | Recent runs to print before watching. Defaults to `10`. |
| `--interval ` | No | Seconds between polls. Defaults to `3`. |
---
# MCP Servers (/cli/mcp-servers)
`agentserve mcp-servers` is also spelled `agentserve mcp-server`.
Every command below also accepts the [global options](/cli/commands#global-options).
## Create MCP server [#create-mcp-server]
```bash
agentserve mcp-servers create [options]
```
**Options**
| Option | Required | Description |
| ------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--name ` | Yes | Server display name. |
| `--description ` | No | Optional server description. |
| `--transport ` | No | Transport protocol. Defaults to `streamable-http` on creation. Accepted values: `streamable-http`. |
| `--url ` | Yes | Absolute HTTP or HTTPS endpoint URL without `{{ENV_VAR}}` references. It determines server identity and is immutable: delete and recreate the server to change endpoints. |
| `--auth-type ` | No | Authentication method. When omitted, and no `headers` are sent, registration probes the endpoint once to classify it, falling back to `headers` when the probe fails or the server does not advertise OAuth. A server publishing RFC 9728 metadata is therefore stored as `oauth`, and headers configured afterwards will not authenticate — send this field explicitly to pin the method. Accepted values: `none`, `headers`, `oauth`. |
| `--headers ` | No | Write-only request headers sent to the server. Replaced wholesale rather than merged on update: sending this field drops every stored header it does not repeat. (JSON, or @path / @- to read a file or stdin). |
| `--timeout ` | No | Per-request timeout in milliseconds. Defaults to 30000 on creation. |
| `--retries ` | No | Number of retries per request. Defaults to 3 on creation. |
| `--enabled` | No | Whether workflows can use the server's tools. Defaults to true on creation. |
| `--no-enabled` | No | Send --enabled as false. |
| `--oauth-client-id ` | No | Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization. |
| `--oauth-client-secret ` | No | Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication. (--oauth-client-secret null sends the word, not JSON null). |
## Delete MCP server [#delete-mcp-server]
```bash
agentserve mcp-servers delete [options]
```
**Arguments**
| Argument | Required | Description |
| ------------- | -------- | ----------------------------- |
| `mcpServerId` | Yes | Unique MCP server identifier. |
**Options**
| Option | Required | Description |
| ----------- | -------- | ----------------------- |
| `-y, --yes` | Yes | Confirm this operation. |
## Get MCP server [#get-mcp-server]
```bash
agentserve mcp-servers get
```
**Arguments**
| Argument | Required | Description |
| ------------- | -------- | ----------------------------- |
| `mcpServerId` | Yes | Unique MCP server identifier. |
## List MCP servers [#list-mcp-servers]
```bash
agentserve mcp-servers list [options]
```
**Options**
| Option | Required | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--search ` | No | Case-insensitive substring match against the server name. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
## List MCP server tools [#list-mcp-server-tools]
```bash
agentserve mcp-servers tools list [options]
```
List MCP Server Tools (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ------------- | -------- | ----------------------------- |
| `mcpServerId` | Yes | Unique MCP server identifier. |
**Options**
| Option | Required | Description |
| -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `--refresh` | No | Refresh tools using your credentials. Otherwise results may reuse another workspace member's recent discovery and omit newly added tools. |
| `--no-refresh` | No | Send --refresh as false. |
## Update MCP server [#update-mcp-server]
```bash
agentserve mcp-servers update [options]
```
**Arguments**
| Argument | Required | Description |
| ------------- | -------- | ----------------------------- |
| `mcpServerId` | Yes | Unique MCP server identifier. |
**Options**
| Option | Required | Description |
| ------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--name ` | No | Server display name. |
| `--description ` | No | Optional server description. |
| `--transport ` | No | Transport protocol. Defaults to `streamable-http` on creation. Accepted values: `streamable-http`. |
| `--url ` | No | Immutable server URL. When provided, it must equal the current URL; use delete and create to change endpoints. |
| `--auth-type ` | No | Authentication method. When omitted, and no `headers` are sent, registration probes the endpoint once to classify it, falling back to `headers` when the probe fails or the server does not advertise OAuth. A server publishing RFC 9728 metadata is therefore stored as `oauth`, and headers configured afterwards will not authenticate — send this field explicitly to pin the method. Accepted values: `none`, `headers`, `oauth`. |
| `--headers ` | No | Write-only request headers sent to the server. Replaced wholesale rather than merged on update: sending this field drops every stored header it does not repeat. (JSON, or @path / @- to read a file or stdin). |
| `--timeout ` | No | Per-request timeout in milliseconds. Defaults to 30000 on creation. |
| `--retries ` | No | Number of retries per request. Defaults to 3 on creation. |
| `--enabled` | No | Whether workflows can use the server's tools. Defaults to true on creation. |
| `--no-enabled` | No | Send --enabled as false. |
| `--oauth-client-id ` | No | Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization. |
| `--oauth-client-secret ` | No | Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication. (--oauth-client-secret null sends the word, not JSON null). |
---
# Meta (/cli/meta)
Every command below also accepts the [global options](/cli/commands#global-options).
## Show what this API supports and which limits apply [#show-what-this-api-supports-and-which-limits-apply]
```bash
agentserve meta status
```
---
# Model (/cli/model)
Every command below also accepts the [global options](/cli/commands#global-options).
## Write the published model, or your draft, to agentserve.model.yaml [#write-the-published-model-or-your-draft-to-agentservemodelyaml]
```bash
agentserve model pull [options]
```
Write the published model, or your draft, to agentserve.model.yaml (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------- | -------- | ----------------------------------------------------------------------- |
| `--draft` | No | Pull your own draft instead of the published model. |
| `-f, --file ` | No | File to write; - writes to stdout. Defaults to `agentserve.model.yaml`. |
## Show what the model file would change in the published model or your draft [#show-what-the-model-file-would-change-in-the-published-model-or-your-draft]
```bash
agentserve model diff [options]
```
Show what the model file would change in the published model or your draft (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------- | -------- | -------------------------------------------------------------------------------------- |
| `-f, --file ` | No | Model file to compare; - reads stdin. Defaults to `agentserve.model.yaml`. |
| `--against ` | No | Model to compare with. Accepted values: `published`, `draft`. Defaults to `published`. |
## Write the model file into your draft (starting one if needed); never publishes [#write-the-model-file-into-your-draft-starting-one-if-needed-never-publishes]
```bash
agentserve model push [options]
```
Write the model file into your draft (starting one if needed); never publishes (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------- | -------- | ----------------------------------------------------------------------- |
| `-f, --file ` | No | Model file to push; - reads stdin. Defaults to `agentserve.model.yaml`. |
---
# Organizations (/cli/organizations)
Every command below also accepts the [global options](/cli/commands#global-options).
## Add organization domain [#add-organization-domain]
```bash
agentserve organizations domains add [options]
```
Add Organization Domain (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ---------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--domain ` | Yes | Domain to claim and verify through a DNS TXT record. |
## List organization domains [#list-organization-domains]
```bash
agentserve organizations domains list [options]
```
List Organization Domains (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------------------------------------------ |
| `--organization ` | Yes | Organization identifier. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `domain`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
## Remove organization domain [#remove-organization-domain]
```bash
agentserve organizations domains remove [options]
```
Remove Organization Domain (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ---------- | -------- | ---------------------------------------- |
| `domainId` | Yes | Domain claim owned by this organization. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
| `-y, --yes` | Yes | Confirm this operation. |
## Verify organization domain [#verify-organization-domain]
```bash
agentserve organizations domains verify [options]
```
Verify Organization Domain (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ---------- | -------- | ---------------------------------------- |
| `domainId` | Yes | Domain claim owned by this organization. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
## Cancel organization access request [#cancel-organization-access-request]
```bash
agentserve organizations access-requests cancel [options]
```
Cancel Organization Access Request (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | -------------------------- |
| `requestId` | Yes | Access request identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
## Create organization access request [#create-organization-access-request]
```bash
agentserve organizations access-requests create [options]
```
Create Organization Access Request (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--target ` | Yes | Target returned by Discover Workspace Access Requests or Discover Organization Access Requests. The target must currently be requestable. (JSON, or @path / @- to read a file or stdin). |
| `--reason ` | No | Why the acting user needs this access. |
## Discover organization access requests [#discover-organization-access-requests]
```bash
agentserve organizations access-requests discover [options]
```
Discover Organization Access Requests (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--organization ` | Yes | Organization identifier. |
| `--search ` | No | Case-insensitive substring match against the access item label. |
| `--target-kind ` | No | Category of access to discover. Accepted values: `feature`, `integration`, `provider`, `model`, `tool`, `knowledge_connector`, `file_share_auth`, `chat_deploy_auth`, `usage_limit`. |
| `--state ` | No | Filter by the acting user’s current access. Requestable items can be submitted for review. Accepted values: `allowed`, `requestable`, `unavailable`. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `label`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
## Get organization access request settings [#get-organization-access-request-settings]
```bash
agentserve organizations access-requests settings get [options]
```
Get Organization Access Request Settings (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
## Update organization access request settings [#update-organization-access-request-settings]
```bash
agentserve organizations access-requests settings update [options]
```
Update Organization Access Request Settings (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| -------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--allow-requests ` | Yes | Allow new requests and approvals. Disabling requests preserves history and still allows cancellation and decline. Accepted values: `true`, `false`. |
## List my organization access requests [#list-my-organization-access-requests]
```bash
agentserve organizations access-requests mine [options]
```
List My Organization Access Requests (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--status ` | No | Filter by request status; omit to include all statuses. Accepted values: `pending`, `fulfilled`, `declined`, `cancelled`, `closed`. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `createdAt`, `targetLabel`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
## List organization access requests [#list-organization-access-requests]
```bash
agentserve organizations access-requests list [options]
```
List Organization Access Requests (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--status ` | No | Filter by request status; omit to include all statuses. Accepted values: `pending`, `fulfilled`, `declined`, `cancelled`, `closed`. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `createdAt`, `targetLabel`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
| `--search ` | No | Case-insensitive substring match against the target label or requester name or email. |
## Preview organization access request [#preview-organization-access-request]
```bash
agentserve organizations access-requests preview [options]
```
Preview Organization Access Request (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | -------------------------- |
| `requestId` | Yes | Access request identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
## Resolve organization access request [#resolve-organization-access-request]
```bash
agentserve organizations access-requests resolve [options]
```
Resolve Organization Access Request (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ----------- | -------- | -------------------------- |
| `requestId` | Yes | Access request identifier. |
**Options**
| Option | Required | Description |
| -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--action ` | Yes | apply: Apply the reviewed change to the governing group or member credit cap. decline: Decline the request without changing permissions or credit limits. Accepted values: `apply`, `decline`. |
| `--expected-fingerprint ` | No | Fingerprint from Preview Organization Access Request. Review its changes and impact before applying; a stale preview returns a conflict. Available when action is apply. Required when action is apply. |
| `--new-limit-credits ` | No | Required only for a usage-limit request: a whole-number credit cap greater than the current cap. Omit for permission requests. Available when action is apply. |
| `--reason ` | No | Required explanation for declining this request. Available when action is decline. Required when action is decline. |
## Create organization invitation [#create-organization-invitation]
```bash
agentserve organizations invitations create [options]
```
Create Organization Invitation (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--email ` | Yes | Email address of the person to invite. |
| `--role ` | No | Organization role to offer. Defaults to member; grants no workspace-specific permissions. Accepted values: `member`, `admin`. |
## Get organization invitation [#get-organization-invitation]
```bash
agentserve organizations invitations get [options]
```
Get Organization Invitation (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ---------------------- |
| `invitationId` | Yes | Invitation identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
## List organization invitations [#list-organization-invitations]
```bash
agentserve organizations invitations list [options]
```
List Organization Invitations (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--search ` | No | Case-insensitive substring match against the invitee email. |
| `--status ` | No | Filter by current invitation status. Omit to include all statuses. Accepted values: `pending`, `accepted`, `rejected`, `cancelled`, `expired`. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `email`, `createdAt`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
## List organization invitation workspaces [#list-organization-invitation-workspaces]
```bash
agentserve organizations invitations workspaces [options]
```
List Organization Invitation Workspaces (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ---------------------- |
| `invitationId` | Yes | Invitation identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--search ` | No | Case-insensitive substring match against the workspace name. |
| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `id`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
## Resend organization invitation [#resend-organization-invitation]
```bash
agentserve organizations invitations resend [options]
```
Resend Organization Invitation (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ---------------------- |
| `invitationId` | Yes | Invitation identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
## Revoke organization invitation [#revoke-organization-invitation]
```bash
agentserve organizations invitations revoke [options]
```
Revoke Organization Invitation (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| -------------- | -------- | ---------------------- |
| `invitationId` | Yes | Invitation identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
| `-y, --yes` | Yes | Confirm this operation. |
## Delete SSO provider [#delete-sso-provider]
```bash
agentserve organizations sso providers delete [options]
```
Delete SSO Provider (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ------------ | -------- | ----------------------------- |
| `providerId` | Yes | Identity provider identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
| `-y, --yes` | Yes | Confirm this operation. |
## Get SSO provider [#get-sso-provider]
```bash
agentserve organizations sso providers get [options]
```
Get SSO Provider (OAuth login or personal API key required)
**Arguments**
| Argument | Required | Description |
| ------------ | -------- | ----------------------------- |
| `providerId` | Yes | Identity provider identifier. |
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ------------------------ |
| `--organization ` | Yes | Organization identifier. |
## List SSO providers [#list-sso-providers]
```bash
agentserve organizations sso providers list [options]
```
List SSO Providers (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------- |
| `--organization ` | Yes | Organization identifier. |
| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
| `--sort-by ` | No | Field used to sort the result. Accepted values: `providerId`, `domain`. |
| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
## Save SSO provider [#save-sso-provider]
```bash
agentserve organizations sso providers save [options]
```
Save SSO Provider (OAuth login or personal API key required)
**Options**
| Option | Required | Description |
| ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--organization ` | Yes | Organization identifier. |
| `--provider-type ` | Yes | oidc: Configure an OpenID Connect identity provider. saml: Configure a SAML identity provider. Accepted values: `oidc`, `saml`. |
| `--provider-id ` | Yes | Globally unique provider ID; saving an existing provider replaces its supplied configuration. |
| `--issuer ` | Yes | Identity provider issuer URL. |
| `--domain ` | Yes | Email domain already verified by this organization. |
| `--jit-provisioning-enabled` | No | Allow SSO sign-in to provision organization membership, subject to eligibility and available seats. |
| `--no-jit-provisioning-enabled` | No | Send --jit-provisioning-enabled as false. |
| `--mapping ` | No | Identity-provider claims mapped to user fields. (JSON, or @path / @- to read a file or stdin). |
| `--client-id ` | No | Identity provider client identifier. Available when providerType is oidc. Required when providerType is oidc. |
| `--client-secret ` | No | Write-only OIDC client secret; the redacted marker from providers get preserves an existing secret. Passing it inline exposes it to shell history and process listings. Required when --provider-type is oidc (@path / @- reads a file or stdin verbatim, including trailing newlines; @@value for a literal leading @). |
| `--scopes ` | No | OIDC scopes; offline\_access is omitted. Available when providerType is oidc. (JSON, or @path / @- to read a file or stdin). |
| `--pkce` | No | Use PKCE for the authorization flow. Available when providerType is oidc. |
| `--no-pkce` | No | Send --pkce as false. |
| `--authorization-endpoint ` | No | Optional authorization endpoint; otherwise resolved through issuer discovery. Available when providerType is oidc. |
| `--token-endpoint ` | No | Optional token endpoint; otherwise resolved through issuer discovery. Available when providerType is oidc. |
| `--user-info-endpoint ` | No | Optional UserInfo endpoint. Available when providerType is oidc. |
| `--skip-user-info-endpoint` | No | Read identity claims from the ID token instead of calling UserInfo. Available when providerType is oidc. |
| `--no-skip-user-info-endpoint` | No | Send --skip-user-info-endpoint as false. |
| `--jwks-endpoint ` | No | Optional signing-key endpoint; otherwise resolved through issuer discovery. Available when providerType is oidc. |
| `--entry-point ` | No | Identity provider SAML sign-in endpoint. Available when providerType is saml. Required when providerType is saml. |
| `--cert ` | No | Identity provider signing certificate. Available when providerType is saml. Required when providerType is saml. |
| `--callback-url ` | No | SAML callback URL; defaults to this provider’s Agent Serve callback. Available when providerType is saml. |
| `--audience