Concepts / Triggers & presentation

Triggers & presentation

Your app does not decide which flow to show. It registers a presenter, the server answers with a flow key when a trigger matches, and your app decides whether this is an acceptable moment to interrupt.

Why the server decides

If the host names the flow, the host owns the sequencing — which means every change to when something appears is a code change, and autopilot could never add a flow you had not already written a call site for. With a presenter registered, adding a flow and its trigger is a server-side act. Your app learns about it when it is asked to show it.

This is also what keeps experiments invisible to the client: the SDK never knows a variant exists. It receives an expanded flow and renders it.

When the server is asked

  • When your app comes to the foreground.
  • When you track an event that some rule mentions.
  • When a flow finishes.

At most once a minute, and never at all until you register a presenter. An endpoint that quietly starts being called on every foreground after a version bump is a behaviour change nobody asked for, so registration is the switch.

What a trigger can be built from

event_happenedThey have done a thing.
event_not_happenedThey have not done a thing.
event_countThey have done it n times.
session_indexThis is their nth session.
days_since_installHow long they have had the app.
days_since_last_seenHow long they were away.
subscription_statusFree, trialing, subscribed, lapsed.
trial_ends_withinTheir trial ends inside a window.
billing_retryA payment failed and is being retried.
grace_periodThey are in a billing grace period.
profile_varA value written by an earlier flow or by inference.

Conditions combine, and each trigger carries a frequency cap so a flow cannot become a daily interruption. Each archetype autopilot writes declares which condition kinds its trigger will be built from, so you can read a plan before any authoring has happened.

Your app can always say no

AppRoot.swift
UpliftFunnel.registerPresenter(self)

// The server names the flow. You decide whether now is a good moment.
func presentSheet(flowKey: String, dismissible: Bool) async -> Bool {
    guard !isMidCheckout else { return false }
    sheetFlowKey = flowKey
    return true
}

func presentFullscreen(flowKey: String, dismissible: Bool) async -> Bool {
    guard !isMidCheckout else { return false }
    coverFlowKey = flowKey
    return true
}

Slots, for something that is not an interruption

A presenter is for a screen that takes over. When you want server-decided content inside one of your own screens — a card on a home tab, a banner above a list — place an UpliftFunnelSlot with a slot id instead. The two work together: the same triggers can target either.

Editing triggers yourself

Autopilot writes and activates triggers as part of publishing, and you can see and change all of them in Settings → Triggers, along with a delivery log of what fired, for whom, and whether it was shown or refused. Turning a trigger off does not delete the flow.