Paywall Schema
DSL Reference

DSL Reference

AgentWallie's paywall schema includes a rich DSL (Domain-Specific Language) for conditionals, expressions, gradients, and animations. These features enable dynamic, personalized, and visually engaging paywalls without writing native code.

Conditionals

Conditionals control whether a component is rendered based on runtime data. Add a condition property to any component to gate its visibility.

Syntax

{
  "type": "text",
  "id": "trial_text",
  "props": { "content": "Start your free trial today" },
  "condition": {
    "field": "products.selected.has_trial",
    "operator": "is",
    "value": true
  }
}

Operators

OperatorDescriptionExample
isExact equality{ "field": "user.plan", "operator": "is", "value": "free" }
is_notNot equal{ "field": "user.plan", "operator": "is_not", "value": "premium" }
containsString contains{ "field": "user.email", "operator": "contains", "value": "@company.com" }
gtGreater than{ "field": "device.session_count", "operator": "gt", "value": 5 }
gteGreater than or equal{ "field": "device.session_count", "operator": "gte", "value": 3 }
ltLess than{ "field": "user.age", "operator": "lt", "value": 18 }
lteLess than or equal{ "field": "user.age", "operator": "lte", "value": 65 }
inValue in array{ "field": "device.platform", "operator": "in", "value": ["ios", "macos"] }
not_inValue not in array{ "field": "user.country", "operator": "not_in", "value": ["CN", "RU"] }
existsField exists{ "field": "user.referral_code", "operator": "exists", "value": true }
not_existsField does not exist{ "field": "user.referral_code", "operator": "not_exists", "value": true }

Field Paths

Conditions can reference these field namespaces:

  • products.* -- Product data: products.selected.has_trial, products.selected.price, products.primary.savings_percentage
  • user.* -- User attributes set via SDK: user.plan, user.session_count, user.onboarding_complete
  • device.* -- Device info: device.platform, device.session_count, device.locale
  • theme.* -- Theme tokens: theme.primary, theme.background

Examples

Show a trial CTA only when the selected product has a trial:

{
  "type": "cta_button",
  "id": "trial_cta",
  "props": { "text": "Start Free Trial", "action": "purchase" },
  "condition": { "field": "products.selected.has_trial", "operator": "is", "value": true }
}

Show a special message for returning users:

{
  "type": "text",
  "id": "welcome_back",
  "props": { "content": "Welcome back! Here's a special offer.", "text_style": "headline" },
  "condition": { "field": "device.session_count", "operator": "gte", "value": 5 }
}

Expressions

Expressions use double-brace syntax ({{ }}) to insert dynamic values into text content, style values, and other string properties. Values are resolved at render time.

Syntax

{
  "type": "text",
  "id": "price_display",
  "props": {
    "content": "{{ products.selected.price }}/{{ products.selected.period }}"
  }
}

Available Variables

VariableDescription
theme.*Any theme token: {{ theme.primary }}, {{ theme.background }}
products.selected.priceDisplay price of currently selected product
products.selected.periodBilling period of selected product
products.selected.trial.periodFree trial period (e.g., "7 days")
products.<slot>.pricePrice of a specific slot: {{ products.primary.price }}
products.<slot>.savings_percentageSavings vs monthly price
user.<attribute>User attribute: {{ user.name }}
device.platformDevice platform (ios or android)

Examples in Text

{
  "props": {
    "content": "{{ products.selected.trial.period }} free, then {{ products.selected.price }}/{{ products.selected.period }}"
  }
}

Examples in CTA Buttons

{
  "type": "cta_button",
  "props": {
    "text": "Subscribe for {{ products.selected.price }}",
    "subtitle": "Save {{ products.primary.savings_percentage }}%"
  }
}

Complete Expression Path Reference

All available expression paths, their example resolved values, and descriptions:

PathExampleDescription
products.selected.price$49.99Display price of the currently selected product
products.selected.periodyearBilling period unit (year, month, week, lifetime)
products.selected.period_label/yrShort period label for display
products.selected.price_per_month$4.17Monthly equivalent price
products.selected.trial_period7 daysTrial duration text
products.selected.trial_priceFreePrice during the trial period
products.selected.savings_percentage58Percentage savings vs monthly billing
products.selected.has_trialtrueWhether the product has a free trial
products.selected.labelAnnualProduct display label
products.selected.slotprimaryProduct slot identifier
products.{slot}.price$49.99Price of a specific slot (e.g., products.primary.price)
products.{slot}.periodyearPeriod of a specific slot
products.{slot}.period_label/yrPeriod label of a specific slot
products.{slot}.price_per_month$4.17Monthly equivalent for a specific slot
products.{slot}.trial_period7 daysTrial duration for a specific slot
products.{slot}.trial_priceFreeTrial price for a specific slot
products.{slot}.savings_percentage58Savings percentage for a specific slot
products.{slot}.has_trialtrueWhether a specific slot has a trial
products.{slot}.labelAnnualLabel for a specific slot
user.{key}freeUser attribute value (e.g., user.plan, user.session_count)
theme.{key}#007AFFTheme token value (e.g., theme.primary, theme.corner_radius)

Replace {slot} with a slot name defined in your schema (e.g., primary, secondary, tertiary). Replace {key} with any key defined in user attributes or the theme object.

Expression paths always use snake_case (e.g., price_per_month, has_trial). If you are using mock data with the Paywall Dev Kit, note that mock data files use camelCase -- the CLI maps between the two formats automatically.

Examples in Style Values

{
  "style": {
    "background_color": "{{ theme.primary }}",
    "corner_radius": "{{ theme.corner_radius }}"
  }
}

Gradients

Background gradients add visual depth to components. Use the background_gradient property inside style to apply a gradient behind component content.

Syntax

{
  "style": {
    "background_gradient": {
      "colors": ["#FF6B6B", "#4ECDC4"],
      "direction": "vertical"
    }
  }
}

Properties

PropertyTypeRequiredDescription
colorsstring[]YesArray of 2+ colors (hex or theme refs)
directionstringNoGradient direction (default: "vertical")

Directions

DirectionDescription
verticalTop to bottom
horizontalLeft to right
diagonal_downTop-left to bottom-right
diagonal_upBottom-left to top-right

Using Theme Colors

{
  "style": {
    "background_gradient": {
      "colors": ["{{ theme.primary }}", "{{ theme.secondary }}"],
      "direction": "horizontal"
    }
  }
}

When both background_color and background_gradient are set, the gradient takes precedence.


Animations

Animations bring components to life when they appear on screen. Add an animation property to any component to animate its entrance.

Syntax

{
  "type": "text",
  "id": "title",
  "props": { "content": "Welcome" },
  "animation": {
    "type": "fade_in",
    "duration_ms": 300,
    "delay_ms": 100
  }
}

Properties

PropertyTypeRequiredDescription
typestringYesAnimation type (see below)
duration_msnumberNoDuration in milliseconds (default: 300)
delay_msnumberNoDelay before animation starts (default: 0)

Animation Types

TypeDescriptionBest For
fade_inFades from transparent to opaqueTitles, subtitles, general content
slide_upSlides up from below while fading inFeature lists, cards
slide_in_leftSlides in from the leftSequential content reveals
scale_upScales from small to full sizeCTA buttons, badges
bounceBounces into positionAttention-grabbing elements
pulseContinuously pulses (scale oscillation)Countdown timers, urgency indicators
shakeHorizontal shakeError states, attention alerts
shimmerTranslucent highlight sweeps across the element repeatedlyCTA buttons, promotional cards

Shimmer Animation

The shimmer animation overlays a moving translucent gradient (transparent to white at 12% opacity to transparent) that sweeps horizontally across the element on a repeating loop. It is commonly used on CTA buttons to draw attention without being as aggressive as pulse.

{
  "animation": { "type": "shimmer", "duration_ms": 3000 }
}

Best Practices

  • Use sparingly. Animate 2-3 key elements per paywall, not everything.
  • Keep durations short. 200-500ms for entrance animations. Longer durations feel sluggish.
  • Stagger with delays. Use incrementing delay_ms values (0, 200, 400) for sequential reveals.
  • Match the purpose. fade_in for content, scale_up for CTAs, pulse for urgency.
  • Don't animate legal links. Footer content should appear immediately.

Example: Staggered Reveal

[
  {
    "type": "text",
    "id": "title",
    "props": { "content": "Go Premium" },
    "animation": { "type": "fade_in", "duration_ms": 300 }
  },
  {
    "type": "feature_list",
    "id": "features",
    "props": { "items": [...] },
    "animation": { "type": "slide_up", "duration_ms": 400, "delay_ms": 200 }
  },
  {
    "type": "cta_button",
    "id": "cta",
    "props": { "text": "Subscribe Now" },
    "animation": { "type": "scale_up", "duration_ms": 300, "delay_ms": 500 }
  }
]

Additional Style Properties

glow_color

Adds a colored drop shadow beneath a component, creating a neon glow effect. Most commonly used on cta_button components. The SDK renders this as .shadow(color: glow_color, radius: 15, y: 4) on iOS.

{
  "style": {
    "background_color": "#ff0080",
    "glow_color": "#61FF0080"
  }
}

letter_spacing

Controls character tracking (spacing between letters). Commonly used on uppercase eyebrow labels or section headers for a wide, spaced-out appearance. The SDK applies .tracking(letterSpacing) on iOS.

{
  "style": {
    "letter_spacing": 4,
    "font_size": 11
  }
}

Paywall Settings Extensions

background_gradient

Apply a gradient background to the entire paywall view instead of a solid background_color. When both are set, the gradient takes precedence.

{
  "settings": {
    "background_gradient": {
      "colors": ["#06060f", "#1a0a20"],
      "direction": "vertical"
    }
  }
}

The gradient object uses the same format as component-level background_gradient in style (see Gradients above).

close_button_style

Controls how the close/dismiss button is rendered. Defaults to "icon".

ValueDescription
"icon"Filled circle with an X icon (SF Symbol xmark). This is the default.
"text"Renders as plain text "x Close" in the top-right corner, with no circle background.
{
  "settings": {
    "close_button": true,
    "close_button_style": "text"
  }
}

Next Steps