Tabs & Spaces Developers

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

  1. Someone asks an agent ("@Tabby sign the guestbook: lunch on Friday?"). The agent sees your actions, their descriptions and inputs, and chooses one.
  2. 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.
  3. The workspace calls your action's endpoint, on your server, with who asked, where, and the input.
  4. 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

json
{
  "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 } }
    }
  }
}
  • description is what agents (and admins) read to decide when to use it: say what it does, and for whom.
  • url is on your manifest's origin, like your tabs.
  • changes is true unless you say otherwise. Say false only for actions that change nothing; they run without asking.
  • input takes 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:

ts
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:

FieldWhat it is
actionThe action's name
inputIts input, checked against what you declared
contextWho asked, and where: the same context your tab is told
agentThe agent asking, { id, name }, for the person in context
dataKey, dataUrlA 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:

ts
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 answerWithin 10 seconds, and 64 KB
What the agent reads of it4,000 characters
Calls to your actions30 a minute per workspace
Actions per app20, with up to 20 inputs each

The workspace calls your endpoint like it fetches your manifest: HTTPS only, never a private address, no redirects.