Functions
Your agent is not a form.It is a file in your repo.
Declare the agent and the tools it can call in TypeScript. Deploy the directory. Zavu builds it, holds the secrets, runs it on your events or on a schedule, and reconciles the live agent with what the file says.
- Runtime
- nodejs24
- Pinned at the first deploy and kept, so a later deploy cannot move it under you.
- Timeout
- 1–180 s
- Default 30. Which ceiling actually binds depends on the trigger.
- Memory
- 128 · 256 · 512 · 1024
- MB per invocation. Most tool handlers fit in the smallest.
- Source
- 200 files · 900,000 bytes
- npm packages are declared, not uploaded.
01 — Write it
A directory, an entrypoint, and whatever it imports.
Deploy starts at the entrypoint and follows relative imports. What it reaches ships; what nothing imports stays on your machine.
What actually gets uploaded
Pick a file. The panel says whether it travels with the deploy, and why.
import { defineAgent, defineTool } from "@zavudev/functions"
import { formatOrder, lookupOrder } from "./lib/orders"
import { HOST_PROMPT } from "./prompts/host"
defineAgent({
senderId: process.env.SENDER_ID!,
name: "Bella",
provider: "zavu",
model: "openai/gpt-4o-mini",
prompt: HOST_PROMPT,
})
defineTool({
name: "lookup_order",
description: "Get current status of an order. Use when the customer asks about one.",
parameters: {
type: "object",
properties: { orderId: { type: "string" } },
required: ["orderId"],
},
handler: async ({ orderId }, ctx) => {
ctx.log("lookup", orderId)
return formatOrder(await lookupOrder(orderId))
},
})The entrypoint. Deploy reads it first and walks its relative imports from here. It defaults to index.ts — name a different one with entrypoint over REST, or --source on the CLI.
Refused paths
The CLI and the API are the same surface.
Use whichever fits the way you ship. Nothing here is available through only one of them.
import { defineAgent, defineTool } from "@zavudev/functions"
defineAgent({
senderId: process.env.SENDER_ID!,
name: "Bella",
provider: "zavu",
model: "openai/gpt-4o-mini",
prompt: "You are Bella, host at the restaurant. Be brief.",
})
defineTool({
name: "check_availability",
description: "Get free reservation slots for a date.",
parameters: {
type: "object",
properties: { date: { type: "string" }, partySize: { type: "number" } },
required: ["date", "partySize"],
},
handler: async ({ date, partySize }) => {
return { available: true, slots: ["19:00", "21:00"] }
},
})02 — Ship it
A deploy is a record you can watch, and go back to.
Deploying returns immediately with an id. You poll it. Every version is kept, with the build output that explains it.
Deployment lifecycle
Step through it, or let it run. Break an import to take the other path.
superseded — what a version becomes once a newer one takes over. It stays readable, and rolling back to it is one call.
{
"id": "fnd_8c21ab5f",
"functionId": "fn_4kq2m9x",
"version": 7,
"status": "pending",
"sourceCodeBytes": 18432,
"bundleBytes": null,
"errorMessage": null,
"deployedAt": null,
"createdAt": "2026-03-14T09:41:12.000Z"
}Poll until status is active or failed. Both are terminal.
Rollback
Roll back by naming a deployment id. Its source, dependencies and runtime pin are copied onto the draft and deployed again as a new version, so history stays append-only. Secrets are not rolled back — they are current, not versioned.
Deploy from GitHub
Link a repository and a push to the branch deploys it. The server decides how the link authenticates, not you: with the Zavu GitHub App installed, private repositories work and there is nothing to add to the repository. Without it you get a manual link, and its webhook secret is printed exactly once. Linking does not check the repository against GitHub — an owner/repo the installation cannot see is accepted and fails on the first deploy.
- connection
- app or manual — the server picks
- branch
- only pushes here deploy
- rootDir
- the subdirectory, for monorepos
- autoDeploy
- false keeps the link and ignores pushes
- lastStatus
- deploying, deployed or failed
- lastError
- why the last push did not land
03 — Run it
On your events, or on the clock.
A trigger subscribes the function to an event type, for one sender or for every one of them. The special cron type runs it on a schedule instead.
Trigger builder
Pick event types and senders. The request and the triggers it creates appear on the right, and a cron expression is resolved to its next fire times in UTC.
Event types
Senders
Add the cron event type above to schedule the function instead of subscribing it to a message. It takes a five-field UTC expression, and a function can hold several with different schedules.
{
"eventTypes": [
"message.inbound"
],
"senderIds": [
null
]
}{
"added": 1,
"skipped": 0,
"triggers": [
{
"id": "fnt_0001",
"eventType": "message.inbound",
"senderId": null,
"active": true
}
]
}One trigger per event type × sender. A cron trigger ignores the sender axis, so it counts once however many senders you picked. Duplicates come back as skipped, not created twice.
Worth knowing
The timeout you set is not always the ceiling that binds.
Event and cron runs
Asynchronous. Nobody is waiting on the response, so a long timeout only bounds what a stuck run costs you.
A tool called mid-conversation
Synchronous. The customer's reply waits on your handler, so keep these well under the limit rather than at it.
A function exposed over HTTP
Additionally bound by the platform's HTTP response limit. Raising timeoutSec does not raise that one.
04 — Operate it
Secrets, logs, and an endpoint when you want one.
Secrets go in and do not come back
Listing them returns each key and the last four characters of its value, never the value. Keys are uppercase env-var style, and the AWS_ and LAMBDA_ prefixes are reserved. Setting one marks the function out of sync rather than restarting it underneath you; the next deploy applies it.
An API key you did not have to wire
Creating a function provisions a scoped Zavu key and injects it as ZAVU_API_KEY, so a handler can call the API back without you managing a credential. For broader scope, create a key yourself and set it as a secret.
Logs, filtered and paged
Fetch invocation logs bounded by a time window or narrowed by a filter pattern, and page through them with nextToken. The same output tails from the CLI while you work.
An HTTPS endpoint, on a switch
Turning the public URL on applies to the already-deployed function — no redeploy. Turned off, the stored URL stops serving and is no longer returned, so publicUrl reads null rather than stale.
Write it, deploy it, watch the logs.
Start free, no credit card. The CLI talks to the same API your code will.