Docs

Rules reference

The rules are one JSON document per product. You edit it in the portal, as a form or as JSON; the SDK downloads it in InitializeAsync and evaluates it on the device every time your app calls TrackAsync.

A complete example

{
  "version": 7,
  "triggers": {
    "session_end": {
      "privateAsk": {
        "minOccurrences": 5,
        "minDaysSinceInstall": 3,
        "cooldownDays": 60,
        "skipIfSubmittedWithinDays": 180
      }
    },
    "workout_completed": {
      "storePrompt": {
        "minOccurrences": 3,
        "minDaysSinceInstall": 7,
        "cooldownDays": 120,
        "maxPerYear": 2,
        "when": {
          "appVersion": { "gte": "2.3.0" },
          "plan": { "in": ["solo", "studio"] }
        }
      }
    }
  }
}

A user who has ended five sessions, three or more days after the SDK first ran, sees the private ask — unless they sent a note in the last 180 days. A user who has completed three workouts, a week or more in, on app version 2.3.0 or later and on one of the two named plans, is offered the store prompt — at most twice in any 365 days, and never within 60 days of a note or 7 days of an ask. Here appVersion and plan are attributes the app sets itself in Attributes; conditions read only those, not the AppVersion option.

The document

FieldMeaning
versionSet by the server on every save. You may send the version you last read; if the rules changed since, the save is refused with rules.conflict. Zero means no check. It is also the ETag devices revalidate with.
storeIdsCopied from the product's settings on every save (androidPackage, iosAppId). Anything you send here is ignored; set them on the product page.
triggersAn object: trigger name → what that trigger may cause. A name is lowercase letters, digits and underscores, 1 to 40 characters, and matches what your code passes to TrackAsync.
privateAskOptional, per trigger. When to show the private ask.
storePromptOptional, per trigger. When to ask the platform for its store prompt. A trigger must have at least one of the two.

A product with no rules yet serves {"version":0,"triggers":{}}: nothing asks, nothing fires.

Thresholds

Both blocks take these fields. "Occurrences" are counted on the device, one per TrackAsync call for that trigger, and never reset. "Install" is the first time the SDK ran on that device.

FieldDefaultAllowedMeaning
minOccurrences11–1000The trigger has happened at least this many times.
minDaysSinceInstall00–365At least this many days since install.
cooldownDays301–365At least this many days since this block last acted for this trigger (the ask was shown, or the prompt was asked for).
whennoneup to 10Conditions on your attributes; see below. All must hold.

One field only for privateAsk:

FieldDefaultAllowedMeaning
skipIfSubmittedWithinDays3650–730Do not ask anyone who sent a note, from any trigger, within this many days.

One field only for storePrompt:

FieldDefaultAllowedMeaning
maxPerYearplatform1–3The most store prompts in any rolling 365 days, counting every prompt the app asked for from any trigger. Left out, it is 2 on Android and 3 elsewhere. The app's MaxStorePromptsPerYear option can lower it further.

Conditions

when maps an attribute key to one condition. The attributes are the ones your app sets in Attributes. Each condition has exactly one operator:

OperatorHolds when the attribute…
eqis exactly this text.
inis exactly one of these texts (a non-empty list).
gteis greater than or equal to this value.
lteis less than or equal to this value.

How a decision is made

Each TrackAsync call counts one occurrence, then:

  1. If the loop has already shown an ask or fired a store prompt, nothing else happens for the rest of that loop's life — one action per session.
  2. If the trigger has a privateAsk and all its thresholds and conditions pass, and no note was sent within skipIfSubmittedWithinDays, the ask is requested. The ask always comes first.
  3. Otherwise, if the trigger has a storePrompt and all its thresholds and conditions pass, the engine then checks its fixed quiet periods and the cap, and asks the platform.

The engine's fixed limits apply whatever the rules say:

Limits

A save is refused, with every problem listed, when the document breaks one of these:

The saved document must also be at most 65,536 characters long (rules.too_large). The SDK runs the same validation on a downloaded document and keeps its cached rules if the new one fails.

Reasons

When nothing happens, the Decision's Reason is one of these stable strings, safe to log:

ReasonMeaning
no_rulesNo rules yet, or none downloaded.
unknown_triggerThe rules do not name this trigger.
invalid_triggerThe name is not lowercase letters, digits and underscores, up to 40.
below_min_occurrencesminOccurrences not reached.
too_soon_after_installminDaysSinceInstall not reached.
cooldownInside cooldownDays.
condition_failedA when condition did not hold.
recently_submittedThe user sent a note too recently.
recent_askThe user was shown an ask too recently.
yearly_capThe store-prompt cap for the last 365 days is reached.
already_acted_this_sessionThis loop already asked or prompted.
prompter_unavailableThis platform has no store prompt (the web, desktop).
prompter_declinedThe platform could not be asked.
errorSomething failed inside the loop; it never throws into your app.