Olio in 5 minutes

Olio personalizes iOS onboarding without app updates. The iOS dev wraps screens in a PersonalizableScreen; marketers author variants and launch campaigns from a web dashboard; the SDK fetches the right variant per user at runtime.

This guide takes you from zero to a live targeted campaign. Five sections, each ~5 minutes.


1. Contact us#

Olio is invite-only while we onboard early customers.

Contact us →

Email us with your team name and the iOS app you'd like to integrate. We'll set up your workspace and send back a sign-in slug + access key. Onboarding a team typically takes one business day.


2. Sign in to the dashboard#

Open https://app.tryolio.ai. You'll see the Olio sign-in form.

  • Workspace: the slug we sent you
  • Access key: the token we sent you
  • (Advanced: custom Worker URL — leave empty for production)

After sign-in you land on the Campaigns page. The left sidebar shows your workspace context, primary nav (Screens / Variants / Campaigns / Integrations / Metrics), and a 4-step setup checklist.

The token is stored in your browser's localStorage and never sent to Vercel or anywhere outside your machine.


3. Define your screens (the iOS contract)#

Before authoring variants, the iOS team needs to declare which screens are personalizable and what slots each one has. This is the screen schema — a per-screen JSON document declaring the ordered layout of static iOS chrome and dynamic Olio slots.

Important — the iOS app is the source of truth for which screens exist.

A screen ID only renders if your iOS code declares it via PersonalizableScreen(id: "..."). The dashboard concepts (schema, variants, campaigns, journeys) are the content layered on top of that screen — they don't summon it into existence. If you create a variant for social_proof in the dashboard but social_proof doesn't have a PersonalizableScreen(...) view in iOS, the variant is fetchable but never rendered. If a campaign's journey references a screen ID iOS doesn't know about, the SDK silently skips it.

The workflow: iOS dev declares the screen → marketer authors schema, variants, campaigns, and journeys for that screen ID. Brand-new screens always start with iOS code first.

If your iOS app already uses PersonalizableScreen, run the Claude skill:

In a Claude Code session pointed at your iOS project:

  /olio-export-schema

The skill walks your Swift sources, finds PersonalizableScreen(id: "...") calls, classifies the surrounding view tree (static elements vs dynamic slots), and emits one screen_schema.json per screen — plus an upload command you can run.

Option B: Hand-author via the dashboard#

Navigate to Screens in the sidebar. For each screen you want to personalize, add elements:

  • Dynamic slot: { type: "dynamic", slot: "hero" } — references a slot key your iOS code declares
  • Static placeholder: { type: "static", label: "App logo · 60pt · centered" } — describes iOS-side chrome that surrounds the slots

Order matters — schemas describe top-to-bottom rendering order in the Preview pane.

Why bother#

Without a schema, the dashboard's Preview tab composites slots in arbitrary dict order and shows nothing about the iOS chrome surrounding them. With a schema, marketers see realistic previews and the SDK can warn (in dev builds) when a variant references a slot the schema doesn't declare.


4. Integrate the iOS SDK#

Add the package to your iOS project — Swift Package Manager URL:

https://github.com/SumitOberoi1207/OlioSDK.git

In Xcode: File → Add Package Dependencies → paste the URL. Or in Package.swift:

.package(url: "https://github.com/SumitOberoi1207/OlioSDK.git", from: "1.0.0")

In your app entry point:

import OlioSDK

@main
struct YourApp: App {
    init() {
        // Configure once at app launch.
        // Olio auto-detects MMP SDKs (AppsFlyer, Adjust) and forwards
        // attribution; auto-collects device_type / app_version /
        // days_since_install. No additional iOS-side wiring needed.
        Task {
            let resolver = NetworkVariantResolver(
                configuration: .init(
                    baseURL: URL(string: "https://api.tryolio.ai/<your-tenant>")!
                )
            )
            await Olio.shared.configure(resolver: resolver)
        }
    }

    var body: some Scene { WindowGroup { ContentView() } }
}

Wrap personalizable screens:

struct WelcomeScreen: View {
    var body: some View {
        PersonalizableScreen(id: "welcome") {
            VStack(spacing: 32) {
                MediaSlot(id: "hero") {
                    HeroIcon(systemName: "leaf.fill")  // ← default content
                }
                HeadingSlot(id: "heading") {
                    Text("Find your moment of calm")    // ← default content
                }
                CTAGroupSlot(id: "cta_group") {
                    Button("Get started") { ... }       // ← default content
                }
            }
        }
    }
}

The default closures are what users see when (a) no variant matches, (b) the variant load fails, or (c) the network is down. Olio is fail-open — your app never crashes from a bad variant.


5. Author a variant + launch a campaign#

Author a variant#

Navigate to Variants in the sidebar → + New variant. Pick a Screen and Variant key — the filename is auto-derived as <screen>.<variantKey>.json (e.g. welcome.fb_sleep.json). The Screen picker lists every screen ID iOS has declared a PersonalizableScreen(...) view for; you can't author a variant for a screen iOS doesn't know about.

Use the Form tab to author with typed inputs (HeadingContent, CTAContent, MediaContent, CTAGroupContent) or the JSON tab for power-user editing. The Preview tab renders the variant inside a phone-frame mock with your screen schema's static placeholders composited in.

Save. The variant is now in KV and reachable at:

GET https://api.tryolio.ai/<tenant>/welcome.fb_sleep.json

Launch a campaign#

Navigate to Campaigns+ New campaign. Configure:

  • Name + status: status starts as draft; flip to live when ready
  • Audience: matchers (any combination, all must pass for the campaign to fire):
    • country — Cf-IPCountry header (multi-select)
    • media_source — from MMP (multi-select)
    • device_typeiphone / ipad / mac (multi-select)
    • app_version — semver min / max range
    • days_since_install — integer range
    • referral_id — from af_sub1 for influencer/affiliate flows (multi-select)
    • percentagemax: 0-100 for random rollout via stable hash
  • Schedule: optional startsAt / endsAt ISO8601 bounds
  • Variants: per-screen variant assignment (one variant key per screen)
  • Journey: ordered list of screens shown to this audience, with optional skips per screen

Save and flip status to live. Resolution on every request:

1. Most-specific live campaign whose audience matches the request wins
   (specificity is set-theoretic: a campaign that constrains a strict
   superset of dimensions narrower than another wins for that audience;
   tiebreak by createdAt desc → id asc).
2. If no campaign matches, serve the default variant ('<screen>.json'
   if it exists).

Verify with curl:

curl "https://api.tryolio.ai/<tenant>/welcome.json?id=test_user&ctx_media_source=organic" \
  | jq .variantId

The response headers tell you which path resolved:

  • X-Tryolio-Campaign: <id> — a campaign matched
  • (no X-Tryolio-Campaign) — served the default

Common patterns#

Influencer / affiliate campaigns#

  1. In your MMP (e.g. AppsFlyer), generate a OneLink with a custom af_sub1 parameter:
    https://<your>.onelink.me/abcd?af_sub1=joelovesfitness
    
  2. Share the link with the influencer.
  3. In Olio, create a campaign with audience [{ type: "referral_id", values: ["joelovesfitness"] }] pointing at an influencer-specific variant.
  4. Users who install via Joe's link see Joe's variant; users from Sarah see Sarah's; everyone else sees the default.

A/B test on a paid channel#

  1. Audience: [{ type: "media_source", values: ["facebook_ads"] }, { type: "percentage", max: 50 }]
  2. Variant A on this campaign; Variant B on a parallel campaign with max: 50 over the complementary half — for cleaner stats, flip a coin server-side via the percentage matcher.

Geo-specific copy#

  1. Audience: [{ type: "country", values: ["JP", "KR"] }]
  2. Variant assigns Japanese-localized copy on the welcome / paywall screens.

Day-1 vs day-7 onboarding#

  1. Two campaigns, both targeting app_version: { min: "2.0.0" }.
  2. Day-1 campaign: days_since_install: { min: 0, max: 0 }.
  3. Day-7 campaign: days_since_install: { min: 7, max: 14 }.
  4. Different welcome / encouragement variants per cohort. Audiences are disjoint, so each cohort gets its own campaign — no precedence config needed.

What Olio does NOT do (yet)#

  • Build influencer link infrastructure — we lean on your MMP's OneLink / tracker URLs. Future option for apps without an MMP.
  • Handle iOS Universal Links / deferred deep linking — your MMP does this.
  • Live MMP API integration — currently the dashboard hydrates media_source dropdowns from a CSV upload (download AppsFlyer's installs report, drop into Integrations page). Live REST API integration is on the roadmap.
  • Per-campaign analytics — request counts and conversion tracking are coming. Today the dashboard surfaces no metrics; the Worker emits X-Tryolio-Campaign: <id> response headers that you can pipe into your existing analytics.
  • Audit log — change history is on the roadmap.

Reference#

  • Worker: https://api.tryolio.aiTryolio/Backend/src/worker.ts
  • Dashboard: https://app.tryolio.aiTryolio/Dashboard/
  • iOS SDK: Tryolio/OlioSDK/ (Swift Package)
  • Demo iOS app: Tryolio/OlioDemo/
  • Claude skills:
    • /olio — generate variant payloads from campaign briefs
    • /olio-export-schema — generate screen schemas from Swift sources

For deeper API reference see Tryolio/Dashboard/README.md (provisioning, token rotation) and Tryolio/OlioSDK/Sources/OlioSDK/Container/PersonalizableScreen.swift (slot vocabulary).