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

# Placements

> A placement is a named trigger point in your app. Your app calls the SDK with a placement key and FlowPilot resolves it to a flow.

A **placement** is a named trigger point in your app, for example `onboarding` or `paywall`. Your app calls the SDK with a placement key, and FlowPilot decides which flow (if any) to show at that point based on the placement's default flow, its audience rules, and any active experiment.

Placements are the bridge between the dashboard and the SDK. They are the most important dashboard concept for anyone integrating the SDK, because they decouple "where in my app" from "which flow runs there". You can swap the flow a placement serves at any time, without shipping a new app release.

## The mental model

There are two sides to every placement, and they are linked by a single string, the **placement key**.

* **In your app code**, you trigger a placement by its key:

  ```swift theme={null}
  try await FlowPilot.shared?.presentPlacement("onboarding", from: viewController)
  ```

* **In the dashboard**, a placement with the matching key holds the configuration that decides what `"onboarding"` resolves to.

The key is the contract. As long as the string in your code matches the placement key in the dashboard (and they belong to the same app), you control the experience entirely from the dashboard.

<Note>
  The dashboard's **SDK Integration** card shows a short snippet like `FlowPilot.trigger("onboarding")`. That is shorthand for the real iOS call, which is `presentPlacement(_:from:)`. See [Presenting placements](/ios-sdk/presenting-placements) for the actual API.
</Note>

## What a placement holds

| Field                            | What it is                                                                                                                                                |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Placement ID** (key)           | The string your app passes to the SDK. Lowercase letters, numbers, and underscores. Unique within an app.                                                 |
| **Platforms**                    | The platforms this placement applies to: **iOS**, **Android**, **Web**. The device platform must be in this list to resolve a flow.                       |
| **Default flow**                 | The published flow version served when nothing else takes priority. Stored as a `default_flow_version_id` (an exact published version, not "the latest"). |
| **Audience filter** (optional)   | Rules that decide whether a user qualifies. No filter means everyone qualifies. See [Audience targeting](/dashboard/audience-targeting).                  |
| **Active experiment** (optional) | A running A/B test bound to the placement. When present and running, it can serve a variant flow instead of the default.                                  |
| **Status**                       | `active` or `paused`. A paused placement resolves to nothing.                                                                                             |
| **Description** (optional)       | A note for your team. Not shown to users.                                                                                                                 |

## How resolution works

When the SDK asks about a placement key, the backend runs this sequence. The first step that fails returns no flow.

<Steps>
  <Step title="Find the placement">
    Look up the placement by app and key. If it does not exist, or its status is **paused**, resolution stops and no flow is returned.
  </Step>

  <Step title="Match the platform">
    The device's platform (`ios`, `android`, or `web`) must be in the placement's platforms list. If it is not, no flow is returned.
  </Step>

  <Step title="Check the audience filter">
    If the placement has an audience filter, the user's attributes must match it. If they do not, no flow is returned. No filter means everyone passes this step.
  </Step>

  <Step title="Apply an active experiment">
    If a **running** experiment is bound to the placement, the user is assigned to a variant (sticky, the same user always gets the same variant). That variant's flow version is served. The experiment can have its own audience filter that gates enrollment.
  </Step>

  <Step title="Fall back to the default flow">
    If no experiment applies, the placement's default flow version is served.
  </Step>

  <Step title="Return the flow">
    The backend returns the flow schema along with the media base URL, any custom fonts, and a cache TTL. If the resolved flow version is missing, no flow is returned.
  </Step>
</Steps>

When no flow is returned, the SDK shows nothing for that placement, and the backend records a `resolve_no_flow` event. For the exact request and response contract, see [SDK REST API](/reference/sdk-rest-api).

<Warning>
  A placement returns **no flow** in any of these cases: it is paused, the device platform is not in its platforms list, the user does not match the audience filter, or it has no default flow and no matching experiment. Plan a native fallback in your app for the "no flow" case. The non-throwing `presentPlacement(_:from:fallback:)` API exists for exactly this. See [Presenting placements](/ios-sdk/presenting-placements).
</Warning>

## The Placements list

Open **Placements** from the sidebar. The page is titled **Placements** with the subtitle "Manage how flows and experiments are triggered in your app". The list is scoped to the app you currently have selected.

* **New Placement**: the button in the page header opens the create wizard. See [Creating a placement](/dashboard/creating-placements).
* **Search**: the **Search placements by ID or description** box filters by a case-insensitive match on the placement ID or description.
* **Status tabs**: **All**, **Active**, **Paused**.
* **Platform filter**: **All Platforms**, **iOS**, **Android**, **Web**. This matches placements that include the chosen platform.

The table has four columns: **Placement ID** (a mono link to the detail page, with the description below it), **Status**, **Platforms**, and **Actions**.

The **Status** column shows one of three badges:

* **Testing** (with a flask icon) when an experiment is bound to the placement.
* **Active** when the placement is active and has no experiment.
* **Paused** when the placement is paused.

Each row's **More actions** (`...`) menu has **View Details**, **Edit** (opens the placement in edit mode), and **Delete**. Clicking the placement ID also opens the detail page.

<Note>TODO: screenshot of the Placements list (header, filter bar, status badges, and the actions menu).</Note>

## Common mistakes

* **Key mismatch.** The placement key in your code must exactly match the placement ID in the dashboard. `onboarding` and `Onboarding` are different. The dashboard lowercases IDs for you, so match that in code.
* **Wrong app.** Placements are app-scoped. If your app's SDK is configured with a different `appId` than the app that holds the placement, it will not resolve. Confirm the app switcher shows the right app.
* **Platform not included.** A placement with only **Android** selected returns no flow to an iOS device (and vice versa). Make sure every platform your app runs on is in the platforms list: **iOS** for the iOS SDK, **iOS** and/or **Android** for an Expo app.
* **Paused placement.** A paused placement always resolves to nothing. Re-activate it from the detail page when you are ready.

## Related pages

* [Creating a placement](/dashboard/creating-placements)
* [Audience targeting](/dashboard/audience-targeting)
* [Placement analytics](/dashboard/placement-analytics)
* [Presenting placements (iOS)](/ios-sdk/presenting-placements)
* [SDK REST API](/reference/sdk-rest-api)
