Building an agent that acts on your app
A programmable agent is a server-side AI “virtual user” that lives in your
app’s Spaces. It reads and replies to messages on the
accelerator, and — once you grant it tools — can act on your app: query the
database, call your functions, and resolve channels. This
page walks the path from a chat-only persona to an agent that does work. The
client.agents reference has the exhaustive API.
1. Start with a persona
Section titled “1. Start with a persona”The minimum agent is a handle, a model, and a system prompt. Triggers decide
when it speaks — start with mention so it only replies when @-mentioned, which
keeps cost bounded:
const { config } = await client.agents.create(appId, { handle: "@assistant", displayName: "Assistant", model: "meta/llama-3.1-8b-instruct", systemPrompt: "You are a concise, friendly teammate in this Space.", triggers: [{ type: "mention" }],});
await client.agents.enable(appId, config.agentId, channelSpaceId); // per-Space opt-inThat’s a working agent: members @-mention it, it replies in the channel.
2. Tune when it speaks
Section titled “2. Tune when it speaks”Triggers bound both behavior and cost. Pick the loosest one the job actually needs:
mention— only when @-mentioned. The safe default.keyword/regex— fire on matching text (e.g. a/summarycommand).always— every message. Powerful, but pair it with a tightcaps.dailyTokenBudgetcircuit breaker.
The first matching trigger wins. Every agent has a hard caps.dailyTokenBudget
that stops it cold when the day’s spend is reached — set it deliberately.
3. Grant tools so it can act
Section titled “3. Grant tools so it can act”By default an agent’s only output is a chat message. Grant tools and it runs a server-side function-calling loop — querying your database, calling your functions, resolving channels — then summarizes what it did back in the Space:
await client.agents.update(appId, agentId, { model: "openai/gpt-oss-120b", // tools require a function-calling model tools: { enabled: true, db: { mode: "read", tables: ["tasks"] }, // read = query/get; write adds mutations functions: ["openTicket"], // function-name allowlist channels: true, maxIterations: 4, },});The exact columns, function parameters, and the closed list of callable tools
are injected at runtime, so the agent can’t invent tables or tools. Tools are
conservatively gated — the database is read-only and per-table allowlisted unless
you opt into write.
4. Describe your app with decorators (recommended)
Section titled “4. Describe your app with decorators (recommended)”Hand-writing the system prompt and keeping a separate tool allowlist in sync is
error-prone. Instead, declare your app’s agent-facing surface in code and
eject both. Annotate a plain class with @MuhkooAgent and its members with
@MuhkooSpace, @MuhkooDB, and @MuhkooFunction:
import { MuhkooAgent, MuhkooSpace, MuhkooDB, MuhkooFunction, ejectAgentPrompt, ejectAgentTools,} from "@muhkoo/connect";
@MuhkooAgent({ name: "Chat Assistant", purpose: "A helpful assistant inside a real-time team chat.", guidance: ["Keep replies short.", "Only chime in when relevant."],})class ChatAssistant { @MuhkooSpace({ description: "Main team discussion." }) general!: string; @MuhkooDB({ access: "read", description: "Chat message history." }) messages!: unknown; @MuhkooFunction({ description: "Open a support ticket." }) openTicket!: () => void;}
await client.agents.create(appId, { handle: "@assistant", displayName: "Assistant", model: "openai/gpt-oss-120b", systemPrompt: ejectAgentPrompt(ChatAssistant), tools: ejectAgentTools(ChatAssistant),});The ejected prompt carries the semantic layer — what the app is, how to
behave, and what each channel, table, and function is for. The runtime supplies
the authoritative schema and tool list separately, so the prompt never restates
columns and can’t drift from your deployed app. The member name is the default
channel/table/function name; override it with name/table.
The decorators only describe; they never change runtime behavior, so the
descriptor class can be a throwaway. They require experimentalDecorators in your
tsconfig.json (no reflect-metadata needed).
Cost & models
Section titled “Cost & models”Inference is metered as the ai_inference axis, billed in neurons so each
model’s true cost is reflected — which is why every agent has a daily token
budget. Pick any model from the edge catalog (GET /api/apps/agent-models lists
all of them with per-model pricing); tool-use needs a function-calling model.
Where to go next
Section titled “Where to go next”client.agentsreference — every method,AgentConfig, the full tool-use config, and triggers.- Writing serverless functions — author the functions an agent calls as tools.