Rheo documentation
Developer Guide

Rheo Manifest Agent Profile

LLM-friendly manifest rules for agents generating Rheo FlowManifest JSON.

Rheo Manifest Agent Profile

Profile version: 2026-05-22 Manifest schema version: 7 Audience: AI agents generating Rheo FlowManifest JSON.

Use this page as current generation guidance. It is not the final validator. Generated manifests must still pass the Rheo agent skill's local validation and Rheo dashboard import validation.

Audit-First Import

Before generating a manifest, agents should run the Rheo skill's audit-import command against your flow entry file (for example app/onboarding.tsx) and write an audit report.

The audit is evidence, not a manifest generator. Use it to decide regions, style tokens, screen backgrounds, local assets, Lottie files, motion, and follow-up questions. If the audit cannot run, explain why and manually cover the same sections.

When intake question 6 is yes, request animation suggestions in the audit and follow animation import rules in the agent skill reference.

Mandatory intake (blocking): Ask every question below and record answers in chat before audit or manifest generation. Do not skip when the repo looks obvious.

  1. Which file, route, or coordinator starts the existing flow?
  2. What is the business purpose of this flow in one sentence?
  3. Should the import optimize for visual fidelity or editable structure?
  4. If source code and screenshots disagree, which is more current?
  5. Which steps must stay native/host-owned vs approximated in Rheo?
  6. Match motion from the codebase (may differ slightly from Rheo presets)? (yes/no)

Run audit with --entry after question 1 is answered. Apply screen.animations and conservative restingMotion only when question 6 is yes and the workspace plan includes animations (Grow+). After audit, ask targeted follow-up questions when ambiguity changes the manifest materially.

Import Output Contract

Generate one raw JSON FlowManifest, normally saved as rheo-import.manifest.json. If the imported flow uses local media, package the manifest and assets as rheo-import.zip. Asset bundling is mandatory when traced screens reference local images, Lottie JSON, or videos. Do not include Markdown, comments, source code, private URLs, API keys, SDK keys, tokens, or credentials.

ZIP bundles must contain:

  • rheo-import.manifest.json
  • rheo-import.assets.json
  • local files under assets/

Use valid placeholder UUIDs in media.mediaAssetId; the dashboard replaces them with uploaded Rheo media asset ids.

Agents must not finish with JSON-only output until they have audited the traced flow for local media references. JSON-only output is acceptable only when the final response states no local assets found.

Required Top-Level Fields

  • flowId — use a placeholder UUID outside Rheo; dashboard import replaces it.
  • schemaVersion7.
  • version — normally 1.
  • defaultLocale — usually en.
  • locales — include defaultLocale.
  • entryScreenId — must point to an existing screen, decision, or external surface.
  • theme — object; use brand colors only when known.
  • screens — screen nodes.
  • decisionNodes — use [] when none.
  • externalSurfaceNodes — use [] when none.
  • sdkAttributeKeys — use [] when none.

ID Rules

  • Screen ids: scr_*
  • Layer ids: lyr_*
  • Decision ids: dec_*
  • External surface ids: surf_*

Prefer readable ids such as scr_welcome, lyr_welcome_title, dec_platform, and surf_paywall.

Flow jump targets

FlowGraphNodeJumpTarget is any non-null graph hop id: scr_*, dec_*, or surf_*. Use it anywhere the manifest routes to another graph node (not flow end).

LocationFieldAllowed targets
screen.next.defaultlinear Continue / auto-advancescr_* | dec_* | surf_* | null (end)
single_choice / multiple_choice .branching.conditions[]goToscr_* | dec_* | surf_*
button go_to_stepscreenIdscr_* | dec_* | surf_*
loader / lottie / video onComplete (mode screen)screenIdscr_* | dec_* | surf_*
request_os_permission outcomesgranted / denied / blockedscr_* | dec_* | surf_* | "continue" | "end"
Decision node casesnext, elseNextscr_* | dec_* | surf_* | null
External surfaceoutcomes, fallbackscr_* | dec_* | surf_*

Screen-only fallbacks: go_back_one_screen.fallbackScreenId and back_button.fallbackScreenId remain scr_* only (history navigation).

Routing pick: same-screen questionnaire taps → choice layer branching; cross-screen field/platform/locale logic after leaving a screen → dec_* decision nodes on screen.next.default.

Layer Kinds

Use only these layer kinds:

stack, text, image, lottie, video, icon, button, back_button, progress, loader, counter, single_choice, multiple_choice, text_input, scale_input, wheel_picker, date_time_input, number_stepper, number_stepper_button, number_stepper_value, phone_input, address_input, oauth_provider, oauth_login, email_password_auth, email_password_field, email_password_submit, carousel, hyperlink, checkbox, conditional

Screen Pattern

Use header/body/footer regions when the source flow implies them:

  • regions.header — top chrome: back button, close button, step title, progress.
  • regions.body — main content and scrollable content.
  • regions.footer — sticky bottom CTA area, legal copy, terms checkboxes.

Audit evidence examples:

  • BackButton, CloseButton, Toolbar, OnboardingHeader, ProgressBar → use regions.header.
  • StickyFooter, BottomActions, bottom safe-area wrappers, buttons outside ScrollView → use regions.footer.
  • ScrollView, FlatList, and main copy/media groups → use regions.body.

Prefer one body stack per screen when there is no obvious header or footer:

{
  "id": "scr_welcome",
  "name": "Welcome",
  "regions": {
    "body": {
      "id": "lyr_welcome_body",
      "kind": "stack",
      "direction": "vertical",
      "gap": 16,
      "style": { "padding": { "t": 24, "r": 20, "b": 24, "l": 20 } },
      "children": []
    }
  },
  "next": { "default": null }
}

Layout sizing

Set explicit style.width and style.height on every layer:

AxisValues
Width"full" (fill parent — never "fill" on width), "auto" (hug), fractions ("1/2", "1/3", "2/3", "1/4", "3/4"), or px
Height"fill" (or legacy "full"), "auto" (hug), the same fractions, or px
Optional clampsminWidth / maxWidth / minHeight / maxHeight (px)

Fill chains: When a region must expand (split tap zones, scroll areas, carousel slide bodies), set height: "fill" on every stack ancestor up to the body root. The body region root stack fills its slot by default.

Hug: Cards and buttons that should size to content use height: "auto". Hug is never upgraded to fill because a parent uses stretch alignment.

Stack packing: Use distribution (start | center | end | between | around). Do not emit legacy justify (schema preprocess migrates it away; new manifests must write distribution).

Icons: Default glyph box is style.width / style.height 24 unless the design specifies another size.

Images: Only set style.radius when the source has a corner radius — do not invent a default radius.

Responsive maps: Prefer styleBreakpoints (sm2xl). Choice option stacks may use selectedStyleBreakpoints. Kind-prefixed layout maps when needed: stackLayoutBreakpoints, buttonLayoutBreakpoints, hyperlinkLayoutBreakpoints, authLayoutBreakpoints, choiceLayoutBreakpoints, carouselLayoutBreakpoints.

Text

  • Use text: { "default": "..." }.
  • Put translations under text.translations.
  • Use numeric fontWeight values such as 400, 600, or 700.
  • Use style.align, not textAlign.

Buttons And Actions

  • Button labels are nested text layers.
  • Good first-draft actions: continue, skip, end_flow.
  • request_app_review is allowed in manifests but agents should not emit it by default (dashboard AI flow gen also excludes it). Use only when the user explicitly wants an in-app store review CTA; requires screen.next.default.
  • Use navigation actions only when the source flow clearly jumps to a specific step.
  • If a CTA sits in a bottom safe-area/sticky container in source, put it in regions.footer.
  • If a back or close control sits above content, use back_button in regions.header.
{
  "id": "lyr_continue",
  "kind": "button",
  "variant": "primary",
  "action": { "kind": "continue" },
  "direction": "horizontal",
  "align": "center",
  "distribution": "center",
  "children": [
    {
      "id": "lyr_continue_text",
      "kind": "text",
      "text": { "default": "Continue" }
    }
  ]
}

Inputs

  • Use at most one input layer kind per active path (single_choice, multiple_choice, text_input, scale_input, wheel_picker, date_time_input, number_stepper, phone_input, address_input). Without a conditional layer that means one per screen; sibling conditional branches may each own their own input because only one branch ever renders.
  • Use stable snake_case fieldKey values.
  • text_input needs classification set to safe or sensitive.
  • text_input.fieldStyle is typography only (fontFamily, fontSize, fontWeight, color, align, lineHeight, letterSpacing, opacity). Padding, radius, background, and border belong on the outer style / styleBreakpoints.
  • wheel_picker captures a string value (mode: "options" or "date" with datePart). Row typography uses itemStyle / selectedItemStyle; chrome stays on outer style / styleBreakpoints. No layer .branching — route later via dec_* with predicate type: "string". Needs a Continue button.
  • date_time_input collects a date, time, or datetime string. Set mode to "date", "time", or "datetime". Use min/max for bounds and defaultValue for prefill. Needs classification (safe/sensitive). Needs a Continue button.
  • number_stepper collects a numeric value via increment/decrement controls. Set min, max, step, and optionally defaultValue and unitLabel. Structural children are number_stepper_button (roles decrement / increment) and number_stepper_value. Enclosing layout matches a stack (direction, align, distribution, gap, outer style). Needs a Continue button.
  • phone_input collects a phone number split into country code and national number. Set defaultCountryCode (ISO 3166-1 alpha-2, e.g. "US"). Needs classification (safe/sensitive). Needs a Continue button.
  • address_input collects a postal address. Configure showLine2, showRegion, requiredFields, and defaultCountryCode. Needs a Continue button.
  • When source uses in-screen pagers (infoSteps, horizontal translateX, pagingEnabled lists), emit kind: "carousel" with one vertical stack slide per page. Do not collapse multi-slide routes to a single static screen.

Choice option styling (single_choice / multiple_choice)

Each selectable option is a child stack on the input layer. Bake default chrome into style (padding, radius, background, border) and map selected UI to selectedStyle on the same stack. Optional selectedStyleBreakpoints when selected chrome changes by viewport. The renderer merges selectedStyle over style when the option is active — do not rely on runtime injection of missing chrome.

When source uses selected === value ? '…selected classes…' : '…default classes…', do not import only the default branch.

{
  "id": "lyr_goal_a",
  "kind": "stack",
  "style": {
    "border": { "width": 1, "color": "#E5E7EB" },
    "background": "#F9FAFB",
    "radius": 12,
    "padding": 16
  },
  "selectedStyle": {
    "border": { "width": 1, "color": "#6D5DF6" },
    "background": "#6D5DF610"
  },
  "children": [{ "kind": "text", "text": { "default": "Option A" } }]
}

Progress And Header Chrome

  • Use progress layers for visual step progress.
  • Put progress in regions.header when it appears with navigation chrome.
  • Use back_button for back/close-style top controls. Add nested text/icon children only when source UI has visible label or custom glyph semantics.

Style Tokens

Agents must inspect the host codebase before falling back to black-and-white defaults:

  • Tailwind/NativeWind config and CSS variables.
  • theme files, design-token files, color palettes, and typography constants.
  • shared primitives such as Button, Text, Typography, Screen, Header, Footer.
  • React Native StyleSheet.create constants.

Map clear values into manifest.theme, layer style, button variants, spacing, border radius, and progress colors. Prefer established tokens over arbitrary colors.

  • Set style.color on text layers when screens use dark or saturated containerStyle.backgroundFill (e.g. #FFFFFF on red/teal education screens).
  • Map StyleSheet.create, Tailwind classes, and shared button/text primitives — not only standalone theme files.

Black-and-white fallback is acceptable only when the audit finds no style-token evidence and the user confirms there is no theme source.

Screen Backgrounds

Screen-level styling is not always a layer. Audit for:

  • LinearGradient, expo-linear-gradient, CAGradientLayer
  • ImageBackground, background image constants, background videos
  • screen shell components such as Screen, Container, OnboardingScreenShell
  • backgroundColor, background, bg-*, CSS variables, and scrim overlays

Map clear screen-level colors to screen.containerStyle.backgroundFill or manifest.theme.background. Map image/video backgrounds to screen background media fills and bundle those assets.

Gradients: Rheo accepts CSS gradient strings on backgroundFill.color when kind is color, for example:

"containerStyle": {
  "backgroundFill": {
    "kind": "color",
    "color": "linear-gradient(180deg, #CCFBF1 0%, #F5F5F4 100%)"
  }
}

Apply shell gradients (e.g. shared BackgroundGradient) to every default onboarding screen unless a screen overrides with a solid brand color. Do not store multi-stop gradients only in manifest.theme.background.

Carousels

When audit or source shows infoSteps, currentInfoStep, horizontal pagers, or dot indicators:

{
  "kind": "carousel",
  "slides": [
    {
      "id": "lyr_slide_1",
      "kind": "stack",
      "direction": "vertical",
      "align": "center",
      "gap": 16,
      "children": []
    }
  ],
  "pageControl": { "position": "bottom" }
}

Bundle every slide asset.

Paging

  • Carousels page by swipe (horizontal snap scroll) by default. Optional pageControl adds dots. The carousel layer itself has no built-in Next/Continue chrome.
  • To add an explicit Next control, use a button layer with action: { "kind": "advance_carousel", "targetLayerId": "<carousel layer id>", "onLast": "noop" | "complete" }. The target must be a carousel on the same screen; onLast defaults to "noop".
  • Do not add regions.footer or body button layers that only bump the pager index without advance_carousel.
  • Use regions.footer for a sticky CTA that advances the whole flow (next screen/route), or rely on landing on the last slide when loop is false and there are 2+ slides.
  • screen.next.default is the edge to the next screen after carousel completion (landing on the last slide via swipe or advance_carousel, or a screen-level Continue on single-slide carousels).
  • Looping carousels never complete through paging, and single-slide carousels ignore advance_carousel — use a continue button to leave the screen.

Layout, alignment, borders, shadows

  • Center hero images: parent vertical stack with align: "center".
  • Text centering: style.align: "center" (not textAlign).
  • Card chrome (rating, testimonials, stacked info cards): wrapping stack with style.background, style.radius, style.padding, style.border, and style.shadow from source — not flat unstyled siblings.

Custom fonts

When source loads custom fonts (Font.loadAsync, useFonts, bundled .ttf/.otf/.woff/.woff2):

  1. Copy font files into assets/fonts/ in the import ZIP.
  2. Add rheo-import.fonts.json with placeholder UUIDs per style (weight + italic).
  3. Set manifest.theme.fontFamily to the primary family name string (e.g. CalSans).
{
  "fontFamilies": [
    {
      "name": "CalSans",
      "styles": [
        {
          "id": "00000000-0000-0000-0000-000000000501",
          "weight": 400,
          "italic": false,
          "path": "assets/fonts/CalSans-Regular.ttf",
          "filename": "CalSans-Regular.ttf"
        }
      ]
    }
  ]
}

On dashboard import, Rheo uploads font files and merges families into app branding. The manifest keeps the family name on theme.fontFamily; runtime resolves uploaded font media from branding.

Assets

Follow local image, Lottie, video, and font references from source code:

  • static imports
  • require(...)
  • asset maps/constants
  • Lottie JSON imports
  • video imports
  • screen background assets
  • shared visual components such as Illustration, Logo, Avatar, Mascot, HeroImage, and ImageBackground
  • platform asset catalogs or bundled asset directories

For each bundled asset:

  1. Generate a stable placeholder UUID.
  2. Use that UUID in media.mediaAssetId.
  3. Copy the file into assets/.
  4. Add an entry to rheo-import.assets.json.
{
  "assets": [
    {
      "id": "00000000-0000-0000-0000-000000000101",
      "path": "assets/hero.png",
      "type": "image",
      "contentType": "image/png",
      "name": "hero.png"
    }
  ]
}

Do not put file paths directly in mediaAssetId.

If any traced screen references local media, the import output must be rheo-import.zip; do not provide only rheo-import.manifest.json. If an asset cannot be copied, report the missing file instead of dropping the media layer.

Lottie detection should include LottieView, lottie-react-native, .lottie, .json animation files, Swift Lottie references, and animation maps/constants.

Before finalizing an import, verify:

  • every traced screen and shared visual component was checked for local assets
  • every local media reference is either bundled or explicitly reported missing
  • every placeholder mediaAssetId appears in rheo-import.assets.json
  • every file listed in rheo-import.assets.json exists in the ZIP

Auth

  • oauth_provider rows must be children of oauth_login.
  • Email/password fields and submit buttons must be children of email_password_auth.
  • Host code must wire auth callbacks; the manifest does not authenticate by itself.

External Surfaces

externalSurfaceNodes are graph nodes (surf_*), not layers. Every node needs fallback. In the flow builder they appear as two add-menu kinds that share this schema:

  • Integration Node — partner providers such as revenuecat or superwall (requires App settings toggle).
  • External Surface Nodeprovider: "headless" (host UI registry; no App settings toggle).

RevenueCat (Integration Node)

{
  "id": "surf_paywall",
  "name": "Integration",
  "config": {
    "provider": "revenuecat",
    "offeringId": "default",
    "presentation": "paywall"
  },
  "outcomes": {
    "purchase_completed": "scr_premium",
    "restore_completed": "scr_premium",
    "dismissed": "scr_free",
    "failed": "scr_free"
  },
  "fallback": "scr_free"
}

The target app must have RevenueCat enabled in Rheo App settings.

Superwall (Integration Node)

{
  "id": "surf_sw_paywall",
  "name": "Integration",
  "config": {
    "provider": "superwall",
    "placementId": "campaign_trigger"
  },
  "outcomes": {
    "purchase_completed": "scr_premium",
    "restore_completed": "scr_premium",
    "dismissed": "scr_free",
    "failed": "scr_free"
  },
  "fallback": "scr_free"
}

The target app must have Superwall enabled in Rheo App settings.

Headless (External Surface Node)

{
  "id": "surf_custom_step",
  "name": "External Surface",
  "config": { "provider": "headless", "hostKey": "onboardingQuiz" },
  "outcomes": {
    "completed": "scr_next",
    "back": "scr_welcome",
    "dismissed": "scr_skip",
    "failed": "scr_skip"
  },
  "fallback": "scr_skip"
}

No App settings toggle. Optional config.hostKey is the externalSurfaces registry key (defaults to the node id). See Headless external surfaces.

Decision Nodes

Use decision nodes for source-code branches. Non-reserved sdk.* keys referenced by decisions must be listed in sdkAttributeKeys.

{
  "id": "dec_platform",
  "name": "Platform",
  "cases": [
    {
      "id": "case_ios",
      "name": "iOS",
      "expression": {
        "kind": "predicate",
        "variable": { "kind": "builtin", "name": "platform" },
        "predicate": { "type": "string", "pred": { "op": "eq", "value": "ios" } }
      },
      "next": "scr_ios_intro"
    }
  ],
  "elseNext": "scr_android_intro"
}

Conditional Layers

Use conditional when one screen shows different layers to different people. Decision nodes pick the next screen; a conditional picks what a screen shows.

Every branch is a direct child stack. Cases are checked in order and the first match wins; elseRootLayerId renders when none match. Case expressions use the same DecisionExpr shape as decision nodes.

{
  "id": "lyr_experience",
  "kind": "conditional",
  "cases": [
    {
      "id": "dc_beginner",
      "name": "Beginners",
      "expression": {
        "kind": "predicate",
        "variable": { "kind": "field", "fieldKey": "experience" },
        "predicate": { "type": "string", "pred": { "op": "eq", "value": "option_1" } }
      },
      "rootLayerId": "lyr_beginner_stack"
    }
  ],
  "elseRootLayerId": "lyr_default_stack",
  "children": [
    { "id": "lyr_beginner_stack", "kind": "stack", "direction": "vertical", "children": [] },
    { "id": "lyr_default_stack", "kind": "stack", "direction": "vertical", "children": [] }
  ]
}

Rules:

  • 1–16 cases, plus a required elseRootLayerId. Every rootLayerId must name a distinct direct child stack.
  • Cases may only read fields answered above them: fields from upstream screens, or same-screen fields whose input layer precedes the conditional in tree order. A case cannot read a field captured inside its own branches.
  • One input, one oauth_login, and one email_password_auth per active path — sibling branches each get their own budget.
  • fieldKey values stay unique across the whole screen, including across sibling branches.
  • Every case needs at least one rule before publishing; drafts may leave a case empty.
  • A conditional owns no style or layout. The winning branch lays out exactly where the conditional sits.

Publish Readiness

The dashboard Publish button runs the same checks as the agent skill's publish-gate audit. Agents must pass these before calling an import complete:

  • The entry target must exist (entryScreenId).
  • A completion path from entry (end_flow, terminal screen next, or external surface end).
  • Every text and icon layer has explicit style.color (including nested button label text — native does not inherit colors).
  • Screens with text_input, multiple_choice, scale_input, wheel_picker, date_time_input, number_stepper, phone_input, or address_input include a button with action.kind: "continue".
  • At most one input layer per active path; do not combine OAuth or email/password login with other inputs on the same path.
  • Every conditional case has at least one rule and reads only fields answered above it.
  • Valid fieldKey (snake_case) on inputs; choice goTo, go_to_step, loader onComplete, and permission outcomes must reference existing scr_*, dec_*, or surf_* ids (or permission sentinels continue / end).
  • External surfaces: real config.provider (revenuecat / superwall for Integration Nodes or headless for External Surface Nodes), connected fallback, matching App settings toggle enabled when a partner provider is used.
  • Run the skill's audit-publish command on rheo-import.manifest.json and fix all blocking issues in the publish-gates report.

Common Invalid Outputs

  • Using unsupported layer kinds.
  • Using label instead of nested text.
  • Omitting decisionNodes, externalSurfaceNodes, or sdkAttributeKeys.
  • Referencing sdk.* decision keys without allowlisting them.
  • Missing external surface fallback.
  • Reusing ids across layers.
  • Generating copy that was not present in source code or screenshots.

Validation

From your app project, ask the agent to run the Rheo skill's validate and audit-publish commands on rheo-import.manifest.json. The bundled scripts are self-contained—no Rheo npm install required.