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
trackan 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_happened | They have done a thing. |
| event_not_happened | They have not done a thing. |
| event_count | They have done it n times. |
| session_index | This is their nth session. |
| days_since_install | How long they have had the app. |
| days_since_last_seen | How long they were away. |
| subscription_status | Free, trialing, subscribed, lapsed. |
| trial_ends_within | Their trial ends inside a window. |
| billing_retry | A payment failed and is being retried. |
| grace_period | They are in a billing grace period. |
| profile_var | A 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
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
}UpliftFunnel.registerPresenter((request) async {
if (!mounted || isMidCheckout) return false;
await showModalBottomSheet<void>(
context: context,
builder: (_) => UpliftFunnelFlow(request.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.