Manifest reference

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

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

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.

FieldTypeRequiredDefaultLimitsDescription
namestringYes—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.
versionstringYes—a semantic version such as 1.4.0 or 2.0.0-beta.1; at most 64 charactersVersion of this build. Each upload needs a new version; installs move between published versions.
titlestringYes—1–80 charactersName people see in the Apps section and on the install screen.
descriptionstringNo—1–160 charactersOne line on what the app is for, shown on its card in the Apps hub.
accentone of blue, violet, teal, green, amber, orange, rose or slateNo——Accent color of the app’s card and tile in the Apps hub.
symbolone of apps, chart, dashboard, table, briefcase, bank, shield, users, inbox, calendar, compass, sparkles, zap, rocket, workflow or documentNo——Symbol shown on the app’s tile in the Apps hub.
iconstringNo—1–512 charactersPath to an image in the bundle, relative to the manifest.
pagesobject[]Yes—1–20 itemsThe pages people open from the Apps section, in order.
pages[].idstringYes—matches ^[a-z][a-z0-9-]{0,62}$Page identifier, unique within the app.
pages[].titlestringYes—1–80 charactersPage name shown in the app’s navigation.
pages[].entrystringYes—1–512 charactersHTML file in the bundle, relative to the manifest, optionally with a #/route for client-side routing (./dist/index.html#/home).
permissionsobjectNosee 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.tablesstring[]No[]at most 50 items; each 1–255 charactersTables the app may list, query and add rows to, by table name.
permissions.modelone of read or noneNo"none"—read lets the app read and query the semantic model.
permissions.workflowsstring[]No[]at most 50 items; each 1–255 charactersDeployed workflows the app may run, by workflow name.
permissions.relaysstring[]No[]at most 50 items; each 1–255 charactersRelays the app may assign tasks to, by Relay name.
permissions.chatone of ask, answer or noneNo"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.filesone of read, write or noneNo"none"—Workspace files: read reads files by id, write also creates new files.
permissions.aiobjectNosee fields—Platform AI the app may call. Every call counts against the app budget.
permissions.ai.modelsstring[]No[]at most 50 items; each 1–255 charactersModel ids the app may name for ai.complete and ai.media. Workspace and permission-group model rules still apply.
permissions.ai.mediastring[]No[]at most 2 itemsMedia the app may generate with ai.media: image, speech.
permissions.knowledgestring[]No[]at most 50 items; each 1–255 charactersKnowledge bases the app may search, by name.
permissions.integrationsstring[]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.
requiresobjectNo——Definitions the install creates when the workspace does not already have them.
requires.tablesstring or string[]No—1–512 characters; at most 50 items; each 1–512 charactersGlob of table definition files (YAML) in the bundle.
requires.rulesstring or string[]No—1–512 characters; at most 50 items; each 1–512 charactersGlob of rule definition files (YAML or JSON) in the bundle.
requires.relaysstring or string[]No—1–512 characters; at most 50 items; each 1–512 charactersGlob of Relay definition files (YAML or JSON) in the bundle.
toolsstring or string[]No—1–512 characters; at most 50 items; each 1–512 charactersGlobs naming your code tools: each <name>.tool.yaml and its <name>.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.
chatBlocksstring, string[] or object[]No—1–512 characters; at most 50 items; each 1–512 charactersChat cards the app provides: a glob of HTML files (each named after its file) or a list of cards.
chatBlocks[].idstringYes—matches ^[a-z][a-z0-9-]{0,62}$Card identifier, unique within the app.
chatBlocks[].titlestringYes—1–80 charactersCard name.
chatBlocks[].entrystringYes—1–512 charactersHTML file in the bundle.
panelsstring, string[] or object[]No—1–512 characters; at most 50 items; each 1–512 charactersDashboard panels the app provides: a glob of HTML files (each named after its file) or a list of panels.
panels[].idstringYes—matches ^[a-z][a-z0-9-]{0,62}$Panel identifier, unique within the app.
panels[].titlestringYes—1–80 charactersPanel name.
panels[].entrystringYes—1–512 charactersHTML file in the bundle.
buildobjectNo——How a Git deploy builds the app. agentserve app deploy builds on your machine and ignores it. See Git deploys.
build.commandstringNo—1–500 charactersCommand run from the repository root after dependencies are installed. Defaults to bun run build.
build.outputstring or string[]No—1–512 characters; 1–20 items; each 1–512 charactersFiles 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.allowVulnerablebooleanNo——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.
budgetobjectNo——Monthly spending cap for the app’s paid work. An admin can override it per install.
budget.monthlyUsdnumberYes—0–1,000,000Cap in US dollars per calendar month (UTC).
triggersobject[]No—at most 20 itemsPlatform 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[].idstringYes—matches ^[a-z][a-z0-9-]{0,62}$Trigger identifier, unique within the app.
triggers[].titlestringYes—1–60 charactersWhat the trigger does, shown on the install review and the Manage page.
triggers[].onobjectYes——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[].handlerstringYes—matches ^[a-z][a-z0-9_]{0,39}$The code tool that handles the event, by tool name.
commandsobject[]No—at most 20 itemsSlash commands added to Chat, such as /call-prep.
commands[].namestringYes—matches ^[a-z][a-z0-9-]{1,31}$What people type after the slash in Chat.
commands[].descriptionstringYes—1–160 charactersShown next to the command in Chat’s slash menu.
commands[].argumentsobject[]No[]at most 5 itemsArguments the command asks for, in order.
commands[].arguments[].namestringYes—matches ^[a-z][a-z0-9_]{0,31}$Argument name, as the handler or page receives it.
commands[].arguments[].descriptionstringYes—1–160 charactersShown as the argument’s hint in Chat.
commands[].arguments[].requiredbooleanNofalse—Whether the command needs it before it runs.
commands[].runobjectYes——{ handler: <tool> } runs a code tool and shows its answer in Chat; { open: { page: <id> } } opens a page, which reads the arguments with launch.get.
actionsobject[]No—at most 20 itemsContext-menu items on files, table rows, workbook queries and dashboard panels, such as “Summarise this file”.
actions[].idstringYes—matches ^[a-z][a-z0-9-]{0,62}$Action identifier, unique within the app.
actions[].titlestringYes—1–60 charactersThe menu item.
actions[].descriptionstringNo—1–160 charactersA short note shown with the item.
actions[].onone of file, table.row, workbook.query or dashboard.panelYes——Where it appears: file, table.row, workbook.query or dashboard.panel.
actions[].typesstring[]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[].tablesstring[]No—at most 50 items; each 1–255 charactersFor table.row: only rows of these tables, each also named in permissions.tables. Defaults to every table there.
actions[].runobjectYes——{ handler: <tool> } runs a code tool with the file, row, query or panel; { open: { page: <id> } } opens a page that reads it with launch.get.
settingsobjectNo——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.typestringNo"object"—Always object.
settings.propertiesobjectYes——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.requiredstring[]No[]at most 30 itemsKeys of the fields an admin must fill in.
connectorsobject[]No—at most 10 itemsMCP 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[].idstringYes—matches ^[a-z][a-z0-9-]{0,62}$How the plugin names the connector in mcp.call.
connectors[].titlestringYes—1–60 charactersShown on the install review and the Manage page.
connectors[].directorystringNo—matches ^[a-z][a-z0-9-]{0,62}$A connectors directory entry, such as linear, which supplies the address and sign-in.
connectors[].urlstringNo—at most 2048 charactersThe remote MCP endpoint (https), when no directory entry is named.
connectors[].authone of none, oauth or headerNo——oauth: an admin connects an account on the Manage page. header: a secret setting is sent as a header. none: no sign-in.
connectors[].headerobjectNo——For a key sent as a header: which secret setting, under which header.
connectors[].header.namestringYes—matches ^[A-Za-z][A-Za-z0-9-]{0,63}$The header, such as Authorization.
connectors[].header.secretstringYes—matches ^[A-Z][A-Z0-9_]{0,63}$The secret setting whose value is sent.
connectors[].header.prefixstringNo—at most 32 charactersSent before the value, such as Bearer .
connectors[].toolsstring[]No—at most 100 items; each 1–128 charactersOnly these tools may be called. Omitted means every tool the server lists.
skillsobject[]No—at most 20 itemsInstruction packs added to the workspace skill library, usable as /name in Chat and by Relays.
skills[].namestringYes—matches ^[a-z0-9]+(-[a-z0-9]+)*$; at most 64 charactersSkill name, as Chat types it after a slash.
skills[].descriptionstringYes—1–1024 charactersWhat the skill is for, shown in the skill library.
skills[].filestringYes—matches \.md$; 1–512 charactersThe Markdown instructions in the bundle.
backgroundobjectNo——Limits for background runs: how many at once and how long each may take.
background.concurrencyintegerNo11–5Most background runs of one install at once.
background.timeoutSecondsintegerNo12010–480Longest one run may take before it is stopped.
background.monthlyUsdnumberNo—0–100,000Suggested monthly cap on background spend, shown on the install review. The admin sets the real cap.
listingobjectNo——What the plugin catalog shows about a first-party plugin.
listing.taglinestringNo—1–120 charactersOne short line under the plugin’s name in Discover.
listing.categoriesstring[]No[]at most 3 itemsCatalog categories, at most three.
listing.packsstring[]No[]at most 5 itemsTeam packs the plugin belongs to: everyone, sales, finance, legal, ops.
listing.screenshotsstring[]No[]at most 8 items; each a .png, .jpg or .webp file; each 1–512 charactersScreenshots in the bundle, shown on the plugin’s detail page.
listing.publisherobjectNo——Who publishes the plugin. Defaults to Agent Serve for first-party plugins.
listing.publisher.namestringYes—1–80 charactersPublisher name.
listing.publisher.urlstringNo—at most 512 charactersPublisher web address.
slotsobjectNo——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.

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

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 ids must be unique.

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.

PermissionWhat it opens
tables: [name, …]List those tables and their columns, query their rows, and add rows.
model: readRead 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: askOpen Chat with a question. The person decides whether to send it.
chat: answerEverything ask opens, and get Chat's answer back in the app. Chat answers read-only: it cannot change anything from there.
files: readRead workspace files by id.
files: writeRead 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.
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

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.

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
FieldWhat it means
kindWhat the admin picks: table, knowledge_base, workflow, relay or model.
title, descriptionWhat the review and the Manage page show next to the picker. Say why the app needs it.
requiredDefaults to true. The app installs only once a required slot is bound, or its default exists.
defaultA 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 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

One YAML file per table:

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 }
FieldRules
nameRequired, 1–255 characters. List the same name under permissions.tables to use the table.
descriptionOptional, up to 2,000 characters.
columns1–200 columns, each with a unique name (1–255 characters, compared without case).
columns[].typeOne of string, number, boolean, date, json, currency.
columns[].requiredOptional true or false.
columns[].uniqueOptional 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

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

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:

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 }
FieldRules
nameRequired, 1–120 characters. List the same name under permissions.relays to use it.
descriptionOptional, up to 2,000 characters.
specializationOptional. Defaults to custom.
instructionsWhat the Relay does and how, in Markdown. Together with delivers, at most 8,000 characters.
deliversWhat 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".
escalationOptional. When it stops and asks a person.
language, exampleTasksOptional. Its working language and up to 10 example tasks.
autonomyobserve (default: drafts only), assist (acts within its rules, asks for the rest) or operate.
approvalsOptional. approvers: installer (default) makes the installing admin its approver; none leaves approvers for later. expireAfterHours is 1–168.
toolsOptional tool groups: workspace_read, knowledge, tables, web, integrations, workflows.
integrations.servicesOptional integration services it may call, by id, such as slack.
workflowsOptional 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.
limitsOptional maxConcurrentTasks (1–10) and maxTasksPerDay (1–200).
responsibilitiesOptional 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

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.
  • 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:

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 names your code tools, such as ./tools/*: each <name>.tool.yaml and its <name>.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.

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, 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 for each section, the handler input and output, and how background runs are authorized.

Build

build tells a Git deploy 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.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.