Guide4 min read
Your first Zenitya tool, from function to agent
Why we package workflows as tools instead of prompts, and a short walkthrough: write one in TypeScript, run it locally, deploy it, and call it from your agent.
Zenitya TeamZenitya
Most teams already have a few workflows they ask an agent to do over and over: find the late orders, pull a customer's invoices, check which deploy broke a metric. Each time, the agent works the steps out again from a prompt. It reads the same docs, makes the same API calls in a slightly different order, and spends tokens getting to an answer that should have been the same as yesterday's.
We built Zenitya around a simple alternative. Write the workflow once as a tool, a TypeScript function with a strict input schema, and let every agent call it. The tool does the same thing on every run, the model only spends tokens deciding when to call it, and the whole team gets it through one MCP connection, whichever agent they prefer.
What a tool is
A Zenitya tool has four parts: a name and description the model reads, an input schema, optional MCP hints such as whether it only reads data, and an execute function. Tools live in a project, which is usually one repository, so billing tools sit next to billing code and go through the same pull requests.
Schemas are deliberately strict. Every property of an object is required and unknown properties are rejected. When a value can be missing, you mark it nullable and the model has to send null on purpose. We made that tradeoff because a model that can leave fields out will eventually leave out the one that mattered, and a rejected call with a clear error is easier to recover from than a quiet wrong answer.
Create a project
Install the CLI, then create a project. The CLI is called zya, and zenitya works as a longer alias.
curl -fsSL https://zenitya.dev/install | sh
zya new order-tools
cd order-tools
bun installThis writes a zenitya.json, a package.json with @zenitya/sdk as the only dependency, and a starter tool in src/index.ts. The config is small on purpose:
{
"$schema": "https://zenitya.dev/schemas/project-v1.json",
"configVersion": 1,
"name": "order-tools",
"entrypoint": "src/index.ts"
}Write the tool
Replace the starter tool with something your team actually asks for. Here is a tool that finds orders by status, calling a function that already exists in the repository:
import { Project } from "@zenitya/sdk";
import { s } from "@zenitya/sdk/schema";
import { findOrders } from "./orders";
const project = new Project();
export default project;
project.tool("searchOrders", {
title: "Search orders",
description: "Find orders by status, newest first.",
schema: s.object({
status: s.enum("open", "late", "shipped").describe("Order status to match."),
limit: s.integer().describe("How many orders to return."),
}),
outputSchema: s.array(
s.object({
id: s.string(),
total: s.number(),
}),
),
hints: { readOnlyHint: true, idempotentHint: true },
execute: async ({ status, limit }, context) => {
context.logger.info({ status, limit }, "Searching orders.");
return findOrders(status, limit);
},
});The enum gives the model exactly three valid statuses, so it cannot invent a fourth. The read-only and idempotent hints tell MCP clients that calling this tool changes nothing and is safe to retry.
Run it locally
Before deploying, run the tool against the source on your machine. The input goes through the same schema validation the deployed copy uses, so a bad input fails here first.
zya exec searchOrders --local --data '{"status":"late","limit":2}'
[{"id":"A-1042","total":31.5},{"id":"A-1039","total":112}]Deploy it
Log in once with an organization machine key, then deploy.
export ZENITYA_API_KEY=... # organization machine key
zya login
zya deploy
zya exec searchOrders --data '{"status":"late","limit":2}'zya deploy sends your source and we build it on our side, so the result does not depend on whose laptop ran the command. The live version only changes once the build passes. The last command calls the deployed copy, which should return the same result you saw locally.
Call it from your agent
Every project's tools join one pool for your organization, served at a single MCP endpoint. In Claude Code, adding it is one command:
claude mcp add --transport http zenitya https://mcp.zenitya.com/acmePeople sign in with OAuth, and scripts or bots use service account keys. What each caller can see is decided by access rules, granted per group, person, or Slack and Discord channel, so the support channel's bot can reach searchOrders without also reaching the billing tools.
The same pool is also available in code mode, where the agent gets one tool with a typed API and writes a short script that chains calls together. Intermediate results stay inside the script instead of passing back through the model. In our own measurements across real team workflows, code mode used 31% fewer tokens, cost 23% less, and had 55% fewer failed calls than calling the same tools one by one.
What to package first
- Something people ask an agent for at least weekly, where the steps are known and the answer should not vary.
- Lookups across internal systems that an agent cannot reach on its own, like a private database or an internal API.
- Anything where you have caught an agent doing it slightly wrong, because a tool fixes it in one place for everyone.
Start with one tool, and add the next one when someone asks an agent the same question twice.
Zenitya Team writes about building tools for agents, MCP, and the practical side of turning a team's workflows into code every agent can call.
zenitya