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.
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 forsocial_proofin the dashboard butsocial_proofdoesn't have aPersonalizableScreen(...)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.
Option A: Generate from iOS code (recommended)#
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 tolivewhen 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_type—iphone/ipad/mac(multi-select)app_version— semvermin/maxrangedays_since_install— integer rangereferral_id— fromaf_sub1for influencer/affiliate flows (multi-select)percentage—max: 0-100for random rollout via stable hash
- Schedule: optional
startsAt/endsAtISO8601 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#
- In your MMP (e.g. AppsFlyer), generate a OneLink with a custom
af_sub1parameter:https://<your>.onelink.me/abcd?af_sub1=joelovesfitness - Share the link with the influencer.
- In Olio, create a campaign with audience
[{ type: "referral_id", values: ["joelovesfitness"] }]pointing at an influencer-specific variant. - 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#
- Audience:
[{ type: "media_source", values: ["facebook_ads"] }, { type: "percentage", max: 50 }] - Variant A on this campaign; Variant B on a parallel campaign with
max: 50over the complementary half — for cleaner stats, flip a coin server-side via the percentage matcher.
Geo-specific copy#
- Audience:
[{ type: "country", values: ["JP", "KR"] }] - Variant assigns Japanese-localized copy on the welcome / paywall screens.
Day-1 vs day-7 onboarding#
- Two campaigns, both targeting
app_version: { min: "2.0.0" }. - Day-1 campaign:
days_since_install: { min: 0, max: 0 }. - Day-7 campaign:
days_since_install: { min: 7, max: 14 }. - 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_sourcedropdowns 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.ai —
Tryolio/Backend/src/worker.ts - Dashboard: https://app.tryolio.ai —
Tryolio/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).