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

A tool is two files in your repository, usually under tools/, sharing a name:

  • <name>.tool.yaml describes the tool: what it does, the input it takes and the output it returns.
  • <name>.ts, .mts, .js, .mjs or .py is the code.

Name them in the manifest with a glob or a list of globs:

agentserve.app.yaml
tools: ./tools/*

The deal desk template ships one tool, score_offer:

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 } }
FieldRequiredMeaning
nameYesLowercase letters, digits and underscores, starting with a letter, at most 40 characters. It must match the file name.
descriptionYesWhat the tool does, up to 1,024 characters. Chat and Relays read it to decide when to call the tool.
inputYesA JSON Schema with type: object. Every call's input is checked against it before the tool runs.
outputYesA JSON Schema with type: object. A result that does not match it fails the run.
effectNoread (default) or write. Use write when the tool changes something outside Agent Serve, such as sending an email or updating another system.
timeoutMsNoTime limit for one run, including starting the sandbox: 1,000 to 300,000. Defaults to 30,000.
packagesNonpm 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-language-server: $schema=https://agentserve.valnoraelric.com/schemas/agentserve.tool.schema.json

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:

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:

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

agentserve app deploy uploads the files your tools globs name together with the build output, and a Git deploy always includes them in the bundle. On upload Agent Serve reads every <name>.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

From your app, with the SDK. An app may always run its own tools; there is no separate permission for them.

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

A tool that calls another system usually needs a key. Store it as a per-install secret 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:

const key = process.env.STRIPE_KEY
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

  • Budget. Tool runs count against the app's monthly budget, 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

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

Limit
Tools per app50
Source file512 KB
input or output schema32 KB each
Input of one call256 KB
Output of one run1 MB
Time per run1 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.