> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstflow.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Agents

> The workspace container that scopes experiences, audience, conversations, keys, and settings and the identity the SDKs route through.

An **agent** is the FirstFlow workspace container for one AI product or environment. Everything you build experiences, audience, conversations, theme, and slash commands lives inside an agent, and every SDK call carries the agent's id so the backend knows which configuration to evaluate.

In code the agent is identified by `agentId` (browser) / `firstflowAgentId` (server). Both refer to the same record; the two names exist because the browser SDK and the server SDK tag their traffic from opposite sides of the runtime.

<Note>
  Most teams create one agent per product, plus a separate agent for staging vs.
  production so test traffic never triggers real experiences. An agent is the
  unit of isolation nothing leaks across agents.
</Note>

## How it works

An agent is not just a folder. It is the **routing key** for the entire runtime cycle. When a message arrives, the backend looks up the agent, evaluates that agent's active experiences and audience, composes a widget, and pushes it to that agent's connected browsers and nothing else.

Three identifiers move through the system, all derived from the same agent record. The browser SDK sends `agentId`; the server SDK tags every observed LLM call with `firstflowAgentId`; the backend resolves both to an internal `workspaceId`, which is the room key for realtime delivery. Get the id wrong on either side and traffic is silently routed to a different agent's experiences.

```mermaid theme={null}
flowchart LR
  subgraph Browser["Browser @firstflow/react"]
    P["FirstflowProvider<br/>agentId + publishableKey"]
  end
  subgraph Server["Your server @firstflow/sdk"]
    O["observe()<br/>firstflowAgentId + sessionId + userId"]
  end
  subgraph FF["FirstFlow backend"]
    R["resolve agent → workspaceId"]
    E["evaluate this agent's<br/>active experiences + audience"]
    C["compose WidgetTree<br/>(Anthropic claude-sonnet-4-6)"]
  end
  O -->|"POST message (202)"| R
  R --> E --> C
  C -->|"widget.show over Socket.IO<br/>room member:{workspaceId}:{userId}"| P
  classDef ff fill:#0d9488,stroke:#0f766e,color:#fff
  classDef cl fill:#6366f1,stroke:#4f46e5,color:#fff
  classDef sv fill:#f97316,stroke:#ea580c,color:#fff
  class R,E,C ff
  class P cl
  class O sv
```

The agent is also the scope of **eligibility**. Triggers, audience, schedule, frequency, and the optional LLM classifier are all stored on experiences that belong to one agent and evaluated server-side against that agent's data. The browser cannot reach across agents, and it cannot force a widget to show it sends `agentId` and reports activity; the backend decides.

## What an agent scopes

Every concept in FirstFlow hangs off exactly one agent. These are the things you create and tune per agent:

* **[Experiences](/concepts/experiences)** the guides, tours, surveys, and announcements you ship. Each has a `type` (`guide | tour | survey | announcement`), a `status` (`draft | active | paused`), a [flow graph](/concepts/flows-and-nodes), and settings.
* **[Audience](/concepts/audience)** the end-users who interact with your product and the [segments](/concepts/triggers-and-audience) you group them into. Audience data is only recorded when the agent's `userTrackingEnabled` flag is on.
* **[Conversations & sessions](/concepts/conversations-and-sessions)** every chat session the server SDK observes, keyed by the `sessionId` you own.
* **Settings** widget theme, AI model assignment, the user-tracking policy, and slash commands (covered below).

An experience belongs to one agent and is invisible to every other agent. Moving a product to a new agent means recreating its experiences; there is no cross-agent share.

## Keys

Each agent exposes two distinct credentials with opposite trust levels. Using the wrong one in the wrong place is the most common setup mistake.

The **publishable key** (`pk_live_…`) is browser-safe and ships in client code alongside `agentId`. It authenticates [`@firstflow/react`](/react/overview) and is not a secret it can only do what a visitor's browser is allowed to do (open a socket, report activity). The **API key** (`ff_live_…`) is a server secret read from the `FIRSTFLOW_API_KEY` environment variable by [`@firstflow/sdk`](/server/overview). It authorizes observed LLM traffic and must never reach the browser.

<Warning>
  Never put the `ff_live_…` API key in client code, a `NEXT_PUBLIC_*` variable,
  or a bundled config. It is a full server credential. The browser only ever
  needs `agentId` + the `pk_live_…` publishable key.
</Warning>

<Snippet file="where-to-find-keys.mdx" />

In a Next.js app the split looks like this the publishable key is inlined into the client provider, the API key stays in server-only environment:

```tsx theme={null}
// app/providers.tsx  (client browser-safe values only)
"use client";
import { FirstflowProvider } from "@firstflow/react";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <FirstflowProvider
      agentId="agt_8f2c…"
      publishableKey="pk_live_…"
      user={{ id: "usr_123", email: "ada@acme.dev" }}
      conversationId={conversationId}
    >
      {children}
    </FirstflowProvider>
  );
}
```

```ts theme={null}
// lib/llm.ts  (server reads the secret from env, never hard-coded)
import OpenAI from "openai";
import { wrapClient } from "@firstflow/sdk/openai";

// FIRSTFLOW_API_KEY (ff_live_…) is read from process.env by the SDK.
export const openai = wrapClient(new OpenAI(), {
  firstflowAgentId: "agt_8f2c…", // same agent as the browser
});
```

The `agt_8f2c…` value is identical on both sides that is what binds the observed conversation on your server to the widgets pushed to the browser. See [Wrap your LLM client](/server/wrap-llm-client) for the full server setup and the [provider reference](/react/provider) for the browser side.

## Settings worth knowing

Agent settings are configuration that applies to **every** experience in the agent, set once on the agent's Settings page rather than per experience.

| Setting        | Field                 | What it controls                                                                                                                    |
| -------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| User tracking  | `userTrackingEnabled` | Whether end-user analytics and audience data are recorded. When off, the backend drops incoming analytics and audience stays empty. |
| Widget theme   | `widget_ui`           | Light/dark appearance tokens applied to every widget the agent renders. See [Theming](/react/theming).                              |
| AI model       | (provider config)     | Which provider powers widget composition and classifier nodes. See [AI Providers](/integrations/ai-providers).                      |
| Slash commands | `commands`            | The command palette users invoke from the chat composer (below).                                                                    |

Theme and slash commands are delivered to the browser automatically: when the provider connects, the backend sends an `agent.config` event and `@firstflow/react` applies them. You do not fetch or pass them they ride the socket.

### Slash commands

Slash commands are agent-level shortcuts a user can trigger from the chat composer. Each `AgentCommand` either launches an experience or fires a host-handled action key:

```ts theme={null}
interface AgentCommand {
  id: string;
  name: string;
  description?: string;
  icon?: string;
  action:
    | { type: "experience"; experienceId: string }
    | { type: "action"; key: string };
}
```

The browser reads them from [`useFirstflow()`](/react/use-firstflow) as `commands` and dispatches one with `triggerCommand(commandId)`. A `type: "experience"` command asks the backend to run that experience's flow for the current user; a `type: "action"` command emits an action `key` your app handles itself. See [Commands](/react/commands) for the rendering and dispatch contract.

## Specific cases and limits

The agent boundary is strict, which removes a few options you might expect:

* **Type is per experience, not per agent.** An agent freely mixes guides, tours, surveys, and announcements. The immutable `guide | tour | survey | announcement` choice is made when you create each experience.
* **You cannot trigger a widget from the client.** Eligibility lives server-side against the agent's experiences. The browser SDK calls `notifyActivity()` and renders what the backend pushes it never decides what shows. This is why client-only integrations cannot force a widget.
* **No cross-agent reuse.** Segments, experiences, taxonomy, and commands are all agent-scoped. Staging and production are separate agents with separate keys and separate data on purpose.
* **`userTrackingEnabled` gates analytics globally.** If it is off, per-experience analytics still won't populate audience or session data for that agent the policy is enforced at ingestion.

When [self-hosting](/self-hosting/overview), the agent model is unchanged; only the host the SDKs talk to differs. Point the browser at your API with the provider's `apiUrl` prop and the server SDK with `FIRSTFLOW_API_BASE_URL`.

## Next

Build something inside the agent with [Experiences](/concepts/experiences) and [Flows & nodes](/concepts/flows-and-nodes), or wire the SDKs via the [browser provider](/react/provider) and [server wrapper](/server/wrap-llm-client).
