Servers and bots

Use the client from a server with an API key, so an app without a browser is persisted and measured like any other.

assistant-cloud has no React dependency and runs wherever JavaScript runs. With an API key, a server is a client in its own right: a Slack or Teams bot, a backend agent, a batch job or a support tool creates threads, stores messages and reports runs, and the dashboard shows them next to your browser traffic.

The client

cloud.ts
import { AssistantCloud } from "assistant-cloud";

export function createCloud(userId: string, workspaceId: string) {
  return new AssistantCloud({
    apiKey: process.env.ASSISTANT_API_KEY!,
    userId,
    workspaceId,
  });
}

The key is created in Settings › API keys and must never reach a browser. The client talks to https://backend.assistant-api.com and acts as the user and workspace you name: the user is who the conversation belongs to and counts as active for the plan, the workspace is who may see the thread. Create the client per user, inside the handler that receives the event.

A conversation, end to end

bot.ts
import { createCloud } from "./cloud";

type SlackMessage = {
  user: string;
  team: string;
  ts: string;
  thread_ts?: string;
  text: string;
};

const threadIds = new Map<string, Promise<string>>(); // Slack thread to cloud thread id; use your database in production.

export async function answer(event: SlackMessage) {
  const cloud = createCloud(event.user, event.team);
  const slackThread = event.thread_ts ?? event.ts;

  let pending = threadIds.get(slackThread);
  if (!pending) {
    pending = cloud.threads
      .create({ last_message_at: new Date(), external_id: slackThread })
      .then(({ thread_id }) => thread_id);
    pending.catch(() => threadIds.delete(slackThread));
    threadIds.set(slackThread, pending);
  }
  const thread_id = await pending;

  const { messages } = await cloud.threads.messages.list(thread_id, { limit: 1 });
  const { message_id: userMessageId } = await cloud.threads.messages.create(
    thread_id,
    {
      parent_id: messages[0]?.id ?? null,
      format: "ai-sdk/v6",
      content: { role: "user", parts: [{ type: "text", text: event.text }] },
    },
  );

  const started = Date.now();
  const reply = await generate(event.text); // Your model call.

  const { message_id } = await cloud.threads.messages.create(thread_id, {
    parent_id: userMessageId,
    format: "ai-sdk/v6",
    content: { role: "assistant", parts: [{ type: "text", text: reply.text }] },
  });

  await cloud.runs.report({
    thread_id,
    message_id,
    status: "completed",
    model_id: reply.modelId,
    provider: "openai",
    input_tokens: reply.usage.inputTokens,
    output_tokens: reply.usage.outputTokens,
    duration_ms: Date.now() - started,
  });

  return reply.text;
}

One thread per Slack thread, then four calls per turn: list the thread's last message, store the user message under it, store the answer, report the run. Keep your own map from the Slack thread to the cloud thread id, because the API creates threads but does not look them up by external_id; the id is still stored on the thread for the dashboard. The map holds the pending creation, so two events of a new thread that arrive together share one cloud thread; it is per process, which is why a real bot keeps the id in its database. The cloud does the rest as it does for a browser: the thread is titled after its first completed run, the user counts as active for the month, and the Threads page renders the conversation from the stored messages.

Set environment and release on the report to filter by deployment, and when the server also exports traces, put the same trace_id on the report so the two halves merge into one run. Every field of the report is on Run reports.

What else the client can do

CallPurpose
cloud.threads.list(), get, update, deleteThe workspace's threads: rename, set metadata, archive, delete.
cloud.threads.messages.list(threadId, { format })Read a thread back in the format you want.
cloud.events.track({ kind, thread_id })Record what the user did; see Engagement.
cloud.scores.create({ name, data_type, value, run_id })Score a run, a thread or a message; see Scores.
cloud.files.generatePresignedUploadUrl({ filename })Upload an attachment to the project's storage.
cloud.projects.threads.list()Every thread of the project, across workspaces. API key only.

The package reference lists every method with its types.

Other languages

The client is a thin layer over the REST API, so a backend in Python, Go or Java does the same calls over HTTP with the key in the Authorization header and the user and workspace in the Aui-User-Id and Aui-Workspace-Id headers. A backend that already emits OpenTelemetry spans can skip the run report: exported spans become runs on their own, and gen_ai.conversation.id on the spans attaches them to the thread. See Other producers.

Reading a project

For reads across the whole project rather than one workspace, the project read API lists runs, usage, threads and scores with the key alone, and the MCP endpoint answers the same questions to an agent.