Build
Actions
An action is something your app does when an agent asks, for the person the agent works for: add a contact, sign a guestbook, look up an order. You declare it in your manifest; your server does it.
How it goes
- Someone asks an agent ("@Tabby sign the guestbook: lunch on Friday?"). The agent sees your actions, their descriptions and inputs, and chooses one.
- If the action changes something, the agent asks first. A card under its answer (and in the person's Inbox) shows your app, the action and its input. Nothing happens until they approve.
- The workspace calls your action's endpoint, on your server, with who asked, where, and the input.
- Your server does it, and answers in a sentence. The agent reads your answer, and the approval card shows it.
An action that only reads runs at once, without asking.
A space's workflows can use your actions as steps too, with the same rule: see Your actions in workflows.
Declaring an action
{
"actions": {
"sign_guestbook": {
"description": "Leave a note in this space's guestbook, for the person you work for.",
"url": "/actions/sign-guestbook",
"input": {
"message": {
"type": "string",
"required": true,
"description": "The note, in their words"
},
"mood": {
"type": "string",
"enum": ["happy", "curious", "busy"],
"description": "How they feel"
}
}
},
"find_order": {
"description": "Look up an order by its number.",
"url": "/actions/find-order",
"changes": false,
"input": { "number": { "type": "string", "required": true } }
}
}
}descriptionis what agents (and admins) read to decide when to use it: say what it does, and for whom.urlis on your manifest's origin, like your tabs.changesistrueunless you say otherwise. Sayfalseonly for actions that change nothing; they run without asking.inputtakes the same field types as collections, and is checked before your server is called.
See The manifest for every rule.
Where an action runs
An action runs in a space where your app has a tab: the agent's space, or one the person names. It knows which tab, so your server can use the data your app keeps there. An agent can't use your app in a space without one of your tabs; it's told where your tabs are.
Answering on your server
The workspace POSTs JSON to your endpoint, with a bearer token:
import {
type ActionRequest,
dataClient,
fetchKeys,
verifyToken,
} from "@tabs-and-spaces/sdk/server";
const WORKSPACE = "https://chat.acme.com";
const keys = await fetchKeys(WORKSPACE);
export async function signGuestbook(request: Request) {
const token = request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "";
const verified = await verifyToken(token, {
keys,
audience: "https://hello.example.com",
issuer: WORKSPACE,
});
if (verified.action !== "sign_guestbook") return new Response(null, { status: 403 });
const body = (await request.json()) as ActionRequest<{ message: string; mood?: string }>;
await dataClient(body).collection("guestbook").create(body.input);
return Response.json({ result: `Signed the ${verified.context.space.name} guestbook.` });
}The request's body:
| Field | What it is |
|---|---|
action | The action's name |
input | Its input, checked against what you declared |
context | Who asked, and where: the same context your tab is told |
agent | The agent asking, { id, name }, for the person in context |
dataKey, dataUrl | A key to your app's data for this request; see below |
Answer 200 with { "result": "…" }: a sentence for the agent to read, or any JSON. Answer an error status with { "error": "…" } to say what went wrong; the agent and the approval card show it.
Your data, from your server
dataKey lets your server read and write your app's data for this request: the same place as the action (its space, its tab, the person who asked), for five minutes. dataClient() takes the request and gives you the same values() and collection() as your page:
const data = dataClient(body);
const notes = await data.collection("guestbook").list({ limit: 10 });
await data.values("user").set("last_signed", new Date().toISOString());Each call is checked again: if the person can no longer see that space, or your tab is gone, the key reaches nothing. Writes need the person to be in the space, as they do from your page. Refusals throw a DataError with a code.
Limits
| Your server's answer | Within 10 seconds, and 64 KB |
| What the agent reads of it | 4,000 characters |
| Calls to your actions | 30 a minute per workspace |
| Actions per app | 20, with up to 20 inputs each |
The workspace calls your endpoint like it fetches your manifest: HTTPS only, never a private address, no redirects.