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

# Integrations

> How FirstFlow connects to the outside world Slack, your webhooks and APIs from inside a flow, and the LLM providers that power its AI features.

FirstFlow integrates with other systems along two distinct surfaces, and keeping
them separate is the key to reading this page. The first surface is the **flow
runtime**: while an experience runs on the backend, its action nodes can reach
out to Slack, your own endpoints, and external APIs server-side, with the flow
graph deciding when. The second surface is the **AI provider** layer: every AI
feature FirstFlow performs (composing a guide's widget, classifying a
conversation, generating a flow) runs on an LLM provider you configure.

Neither surface lives in the browser. The browser SDK is a passive consumer that
renders what the backend pushes over Socket.IO; it never calls Slack, your APIs,
or an LLM directly. That boundary is why outbound integrations can hold secrets
safely and why eligibility and flow navigation are evaluated server-side. The
sections below describe each integration in turn what it does, how it runs, and
the limitations worth knowing before you wire it up.

<Note>
  Outbound integrations (Slack, webhooks, API calls) are about what a **flow
  does** as it runs. AI providers are about what **FirstFlow's own AI**
  runs on. These are configured in different places and fail independently.
</Note>

## How integrations fit the runtime

When a user qualifies for an experience, the backend walks its flow graph from
the trigger, evaluating decisions and running action nodes in order, until it
reaches a result node and composes a widget. Action nodes Slack, Webhook, API
Call execute during that walk, on the server. A node can run **inline** (the
walk waits for it) or be **deferred** until the next step, so a slow webhook
need not block the widget the user is waiting on.

The AI provider layer sits underneath that same walk: a classify node calls your
chosen provider to pick a branch, and the result node calls Anthropic
(`claude-sonnet-4-6`) to render the [`WidgetTree`](/concepts/widgets-and-blocks)
that gets pushed to the browser. The diagram below shows where each integration
hooks into a single flow run.

```mermaid theme={null}
flowchart TD
    T([Trigger: user qualifies]) --> API
    API[API Call node<br/>server-side fetch] -->|response in flow context| C
    C{Classify node<br/>LLM branch} -->|calls AI provider| P[(AI provider<br/>OpenAI / Anthropic / custom)]
    C -->|branch A| SLACK[Slack node<br/>post message / DM]
    C -->|branch B| HOOK[Webhook node<br/>HTTP to your endpoint]
    SLACK --> R
    HOOK --> R
    R[Result node] -->|Anthropic claude-sonnet-4-6| W[[WidgetTree]]
    W -->|pushed over Socket.IO| B([Browser SDK renders])

    classDef ext fill:#7c3aed,stroke:#4c1d95,color:#fff;
    classDef ai fill:#0d9488,stroke:#134e4a,color:#fff;
    classDef flow fill:#ea580c,stroke:#7c2d12,color:#fff;
    class SLACK,HOOK,API ext;
    class P,W ai;
    class C,R,T flow;
```

Every box left of the browser runs on the backend. The browser only enters at
the end, when the finished widget arrives. See
[Flows & nodes](/concepts/flows-and-nodes) for the full node catalog and
[How it works](/how-it-works) for the end-to-end cycle.

## Slack

A **Slack message** node posts a message or DM to your workspace while a flow
runs routing an escalation, alerting a channel when a survey response comes in,
or notifying support that an experience fired. You connect Slack once via OAuth
in the dashboard under **Connections**, pick a default channel, and then add the
node to any flow and configure its destination and content. The node runs
server-side when the walk reaches it, inline or deferred like any action node.

Slack is **optional and degrades gracefully**. If the self-host environment
lacks `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, or `SLACK_SIGNING_SECRET`, the
provider simply isn't offered and nothing else breaks. Full setup, OAuth scopes,
and node configuration live in [Slack](/integrations/slack).

## Webhooks

A **Webhook** node calls *your* backend as the flow runs to sync a survey
answer, kick off a provisioning job, or notify an internal system. FirstFlow
delivers webhooks reliably through its own outbox table and a retry worker with
exponential backoff, so a momentary outage on your side does not drop the event.
When you set a signing secret, each request carries an HMAC signature so you can
verify authenticity. To verify, compute the HMAC of the **raw** request body
with your secret and compare it in constant time to the signature header,
rejecting anything that does not match.

Experiences can also enqueue a webhook programmatically from a logic action
rather than from a node on the canvas. That path is **idempotent**: the first
enqueue is accepted and any duplicate of the same event is ignored, so a retried
client call cannot fire your endpoint twice. Payload schemas, the event catalog,
and verification details are in [Webhooks](/integrations/webhooks).

<Warning>
  Verify the HMAC against the **raw, unparsed** body. Re-serializing the JSON
  before hashing changes the bytes and the signature will never match.
</Warning>

## API calls

An **API Call** node fetches data from an external HTTP endpoint **server-side**
during a flow run and makes the response available in the flow's context, so
downstream nodes including a guide's widget composition can incorporate live
data like account status, plan limits, or order state. Because the call runs on
the backend, your endpoint and credentials are never exposed to the browser.

The natural pairing is an API Call feeding a branch. Fetch the account first,
then route on it with a [classify or decision node](/concepts/flows-and-nodes) —
one guide for trial accounts, another for paid before the result node composes
the final widget. Configuration and the request/response shape are in
[API calls](/integrations/api-calls).

## AI providers

FirstFlow's AI features run on LLM providers you configure once in the dashboard
under **Settings → AI Providers**, where each key is stored encrypted; you then
assign which provider and model power each feature on an agent's settings. The
division of labor is fixed for one feature and configurable for the rest:

| Feature                                     | Provider                        |
| ------------------------------------------- | ------------------------------- |
| Guide **widget composition** (result nodes) | Anthropic (`claude-sonnet-4-6`) |
| **Classify** nodes / classifier triggers    | Your chosen provider & model    |
| **Flow generation** & URL import            | Your chosen provider            |
| Conversation classification                 | Your chosen provider            |

Supported providers are **Anthropic**, **OpenAI**, **Cloudflare** (Workers AI
gateway), and any **OpenAI-compatible** endpoint including self-hosted Ollama,
vLLM, and LM Studio. AI features degrade gracefully: when no key is configured
the rest of the platform keeps running, the affected AI step is skipped, and no
widget is composed for that path. See [AI providers](/integrations/ai-providers)
for per-feature assignment.

This provider layer is **internal to FirstFlow** it is what FirstFlow's backend
uses to *render and route* experiences. It is separate from the LLM client in
**your own product** that the [server SDK](/server/overview) wraps to *observe*
your conversations. Those observed turns are what power user-message triggers and
classifier context; tag every observed call with `firstflowAgentId`, `sessionId`,
and `userId`:

```ts theme={null}
import OpenAI from "openai";
import { wrapOpenAI } from "@firstflow/sdk/openai";

// reads FIRSTFLOW_API_KEY from the environment
const openai = wrapOpenAI(new OpenAI());

const completion = await openai.chat.completions.create({
  model: "gpt-4o-mini",
  messages,
  firstflowAgentId: "agt_…",
  sessionId: "ses_…", // must equal the browser's conversationId
  userId: "user_123",
});
```

The wrapper strips those three identifiers before the request reaches OpenAI. If
any one is missing, the call passes through **unobserved** with a one-time
warning which is why partial tagging silently disables triggers for that
conversation.

## Self-hosting

Slack, the AI providers, and the ClickHouse analytics warehouse are each
optional. When a credential is absent the corresponding capability is simply
unavailable and the rest of the platform continues to work, so you can adopt
integrations incrementally. Set the relevant keys in your environment —
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, the `SLACK_*` secrets and consult
[self-hosting Configuration](/self-hosting/configuration) for the full list.

## Next

Pick the integration you're wiring up next, or step back to the runtime that
drives them in [Flows & nodes](/concepts/flows-and-nodes).

<CardGroup cols={2}>
  <Card title="Slack" icon="slack" href="/integrations/slack">
    Connect via OAuth and post messages or DMs from a flow.
  </Card>

  <Card title="AI providers" icon="microchip" href="/integrations/ai-providers">
    Assign the LLM provider and model behind each AI feature.
  </Card>
</CardGroup>
