<!-- AI agents: this is the Markdown version of a Reception documentation page. Index: /llms.txt -->

# Automations

Send a message in the chat when something happens in your app: an offer when someone closes your paywall, help when a purchase is cancelled, a check-in after onboarding or a rating request after a good moment. You build automations in the dashboard; your app reports the moment with one line.

Automations need Reception iOS SDK 1.5 or later or react-native-reception 1.3 or later. The website chat has no automations.

## How automations work {#how-it-works}

Open **Automations** in the dashboard and start from a template or from scratch. Each automation has four steps:

| Step | What you choose |
| --- | --- |
| Trigger | The event that starts it, such as `paywall_declined`, and whether it runs once per person or every time the event happens. |
| Who | Everyone, people without a subscription or people with one (a one-time purchase in the App Store, such as lifetime access, counts as with one, because the App Store can’t tell lifetime access from other one-time purchases), plus up to four rules on your app’s [metadata](/docs/accounts#metadata), and whether it also reaches people in an ongoing conversation. A subscription event already says who has one, so the choice it rules out is disabled: after `subscription_expired` nobody has a subscription, after `subscription_started` everyone does, while `subscription_cancelled` and `billing_issue` leave access as it was until the period ends. |
| Wait | Right away, after a delay, or at a local time on a later day, and whether the messages come with a push. |
| Action | What happens, in order, from a sender you choose: open the chat (right away only), send a message, send an offer from your [paywall cards](/docs/paywalls), ask for an App Store rating, or wait before the rest goes out. Add as many steps as the moment needs, up to eight, so one automation can open the chat, send an offer, and follow up with a message a day later. |

- **Once per person**, the default, reaches a person’s device once; later events send nothing. **Every time** reaches them at each event, once their previous run of the automation is complete.
- A **Wait** action pauses the flow: the actions before it go out together, and the rest follows when the wait is over, with a push if the automation sends one. The follow-up is checked again before it goes out: it stays unsent when the person replied in the meantime (unless **Also during a conversation** is on), no longer matches the rules, or its offer or rating can’t be sent, and the automation then ends for them.
- An ongoing conversation isn’t interrupted. While a conversation had activity in the last 24 hours, the automation skips the person and reaches them at a later event. Turn on **Also during a conversation** in the Who step to send it anyway; the message then joins that conversation.
- An offer is sent only while the device reports that paywall as available, and a rating request needs your App Store ID and skips devices already asked for a review, by a teammate or an automation. An automation whose offer or rating can’t be sent holds everything back and tries again at a later event, so nobody gets a message that introduces nothing.
- The messages are added to the person’s chat, in order, as unread. The conversation reaches your inbox when they reply.
- Your app sends only events an enabled automation listens for. Other events never leave the device. A listened event registers the device with Reception if launch registration has not succeeded yet.
- Messages sent later arrive as unread, with one push if you choose one. Offers are promotional, so a push with an offer needs the person’s consent in your app’s permission flow (App Store Review Guideline 4.5.4).

Admins create, edit and switch automations; members see them read-only. Editing never sends a message again, and deleting keeps messages already sent.

## Report an event {#track}

Call `track` where the moment happens, after configuring Reception:

```swift
Reception.shared.track("onboarding_completed")
```

In React Native:

```typescript
Reception.track('onboarding_completed');
```

The Trigger step suggests `paywall_declined`, `checkout_cancelled`, `onboarding_completed`, `app_open` (the app comes to the foreground) and `app_close` (it goes to the background); any name works. The name must match the automation’s event exactly and use 1–64 letters, digits, underscores, dots or hyphens. `track` returns at once and never fails: offline, on a server error or when no automation applies, nothing happens.

Pass `opensChat: true` where the chat should open again for people who got the message, such as a closed paywall:

```swift
Reception.shared.track("paywall_declined", opensChat: true)
```

In React Native:

```typescript
Reception.track('paywall_declined', { opensChat: true });
```

The chat opens with the automation’s message when it is sent, and again at later events for people who got it, so they find the message and their conversation again. People the automation doesn’t reach, because its rules exclude them or its message comes later or as a push, aren’t interrupted. The chat only opens while your app is active, the chat is closed and the answer arrives within 5 seconds, so it never shows up late.

The automation’s switch in the dashboard decides both: without an enabled automation for the event, the chat stays closed and nothing is sent. Apps notice a switched automation within 5 minutes.

Right after you create an automation, its overview shows the line to add to your app and a prompt for your coding agent; **Add to app** in the overview’s header brings it back any time.

## Subscription states {#store-events}

Ten events need no code: `trial_started`, `trial_converted`, `trial_cancelled`, `trial_expired`, `subscription_started`, `subscription_renewed`, `subscription_cancelled`, `billing_issue`, `subscription_expired` and `refunded`. They arrive from your subscription provider, RevenueCat, Superwall or Adapty, once it is connected under **Settings → your app → Integrations** (the Trigger step offers them only then), and reach every registered device of the person, right away and whether or not the app is open. See [Subscription providers](/docs/subscriptions) for the setup and what each event means.

## Closed paywalls and cancelled purchases {#paywalls}

Two moments are worth answering right away: someone closes your paywall without buying, or cancels Apple’s purchase sheet. Report both with `opensChat: true`. The **Close paywall** and **Abandon transaction** templates start from these events.

| Moment | Event | Your paywall | The chat |
| --- | --- | --- | --- |
| Paywall closed without a purchase | `paywall_declined` | closes, or comes straight back | opens once the paywall is gone, or above the one that came back |
| Purchase sheet cancelled | `checkout_cancelled` | stays open | opens above the paywall |

When the message has an offer, tapping it opens that paywall through your [paywall card setup](/docs/paywalls). After a cancelled purchase, closing the chat returns the person to your paywall.

### An existing exit offer {#exit-offer}

If your app already shows an offer when the paywall closes or a purchase is abandoned, keep the offer and move the moment into the chat: leave the offering or placement where it is configured, stop presenting it from code at that moment, report the event, and pick that offering as the automation’s offer. It must be among the paywalls your app registers with Reception. With RevenueCat, exit offers only appear through `presentPaywall` and `presentPaywallIfNeeded`; present `PaywallView(offering:)` directly to drop them. Or keep the offer as it is and don’t report that event.

### Your own paywall {#own-paywall}

Report the close from the sheet’s `onDismiss`, so the chat opens after the paywall is gone, and the cancelled purchase where your code learns of it. The chat waits for the purchase sheet to close and for a paywall your app shows again to settle, up to five seconds:

```swift
import Reception
import StoreKit
import SwiftUI

struct UpgradeButton: View {
    @State private var showsPaywall = false
    @State private var purchased = false

    var body: some View {
        Button("Upgrade") {
            purchased = false
            showsPaywall = true
        }
        .sheet(isPresented: $showsPaywall, onDismiss: {
            if !purchased { Reception.shared.track("paywall_declined", opensChat: true) }
        }) {
            PlansView(onPurchase: { purchased = true; showsPaywall = false })
        }
    }
}

// Where your paywall buys a product:
func buy(_ product: Product) async throws -> Bool {
    switch try await product.purchase() {
    case .success(let verification):
        guard case .verified(let transaction) = verification else { return false }
        await transaction.finish()
        return true
    case .userCancelled:
        Reception.shared.track("checkout_cancelled", opensChat: true)
        return false
    default:
        return false
    }
}
```

With RevenueCat purchases, `Purchases.shared.purchase(package:)` reports the cancelled purchase as `userCancelled`.

In React Native, call `Reception.track('paywall_declined', { opensChat: true })` from the `onDismiss` of your paywall’s modal or sheet, and `Reception.track('checkout_cancelled', { opensChat: true })` where your code learns of the cancelled purchase.

### RevenueCat paywalls {#revenuecat}

```swift
.sheet(isPresented: $showsPaywall, onDismiss: {
    if !purchased { Reception.shared.track("paywall_declined", opensChat: true) }
}) {
    PaywallView()
        .onPurchaseCompleted { _ in
            purchased = true
            showsPaywall = false
        }
        .onPurchaseCancelled { Reception.shared.track("checkout_cancelled", opensChat: true) }
}
```

With `presentPaywallIfNeeded`, pass the same calls as `purchaseCompleted`, `purchaseCancelled` and `onDismiss`.

### Superwall {#superwall}

Add these two methods to your `SuperwallDelegate`, or set one if your app has none:

```swift
import Reception
import SuperwallKit

final class PaywallEvents: SuperwallDelegate {
    func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) {
        if case .transactionAbandon = eventInfo.event {
            Reception.shared.track("checkout_cancelled", opensChat: true)
        }
    }

    func didDismissPaywall(withInfo paywallInfo: PaywallInfo) {
        if paywallInfo.closeReason == .manualClose {
            Reception.shared.track("paywall_declined", opensChat: true)
        }
    }
}

// After Superwall.configure:
Superwall.shared.delegate = PaywallEvents()
```

Report the closed paywall from `didDismissPaywall`, not from Superwall’s `paywallDecline` event. Superwall sends `paywallDecline` while the paywall is still on screen, and closing it would take the chat with it. `.manualClose` means the close button or a swipe down; a purchase or a paywall that makes way for another reports a different reason. If a Superwall campaign answers the close with its own offer, or an abandoned checkout with its own paywall, one of the two should own that moment: either don’t report that event, or remove the campaign rule, keep the paywall and its placement, and attach it to the automation as its offer.

### Paywalls that must stay {#hard-paywall}

What follows a closed offer is your app’s decision. If your paywall must stay, for example at the end of your onboarding, show it again when it closes: the chat opens above it once it is back. Show it again too when an offer from the chat closes without a purchase. With Superwall, where `special_offer` is the offer’s placement and `onboarding` the paywall’s:

```swift
func didDismissPaywall(withInfo paywallInfo: PaywallInfo) {
    guard paywallInfo.closeReason == .manualClose else { return }
    if paywallInfo.presentedByPlacementWithName == "special_offer" {
        Superwall.shared.register(placement: "onboarding")
    } else {
        Reception.shared.track("paywall_declined", opensChat: true)
    }
}
```

## Results {#results}

Each automation opens on its overview: how many people got the message, then opened the offer or tapped Rate, replied, and bought within 7 days after it. A purchase after the message isn’t necessarily because of it. **Scheduled** lists messages still waiting for their time, and **Skipped** lists people who matched but haven’t received it yet, with the reason: the offer wasn’t available, they were already asked for a rating, or a conversation was ongoing.

Purchases and the Who step’s subscription choice count both the App Store state the SDK reports and your app’s `subscription_status` [metadata](/docs/accounts#metadata), whichever says the person pays, so web purchases and granted access your app reports as active count too.
