Skip to content

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.

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

That’s a working agent: members @-mention it, it replies in the channel.

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 /summary command).
  • always — every message. Powerful, but pair it with a tight caps.dailyTokenBudget circuit 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.

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.

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

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.