VeveeBlog · 6 min read
Blog · 6 min read

Dynamic onboarding: a different first step for every user

A teacher and a student sign up the same minute. The teacher wants to build a quiz; the student wants to summarize a lecture. Your onboarding shows them both the same five-step checklist. One of them bounces.

Last updated: 2026-06-02

The static checklist is a guess that never updates

Most onboarding is a hardcoded array of steps: create a project, invite a teammate, connect a source, run your first job. It was written once, for an imagined median user, and it never adapts. The problem is that "median user" rarely exists. A teacher signing up to your study app and a student signing up the same minute have almost nothing in common except the button they clicked. Showing them the same five steps means at least one of them is reading instructions for a job they do not have - and that is the moment activation quietly dies.

You already know who they are

By the time someone finishes signup you usually have signal: a role they picked, the plan they landed on, a referral source, the first thing they clicked. If you run behavioral analytics you have even more - the events they fired in their first thirty seconds. The data to personalize the first step is sitting in your analytics profile. What you have been missing is a cheap way to turn "this person is a teacher who just imported a PDF" into a concrete, written next step without hand-coding a decision tree for every persona.

  • user_attributes - the role, goal, or plan they declared at signup
  • user_events - what they actually did in their first session
  • popular_features - what users like them rely on most
  • cohort_compare - where this user sits versus a comparable cohort

Compose turns the profile into a step

A Vevee compose type is a prompt plus the data sources it should read plus a structured output schema, all defined in the dashboard. You write the intent once - "Given this user’s role and what they have done so far, name the single most useful next step and one sentence of encouragement" - toggle on user_attributes and user_events, and define an output schema with a title, a body, and a CTA route. Your code never branches on persona. It asks compose for the next step and renders whatever comes back.

// app/onboarding/next-step/route.ts (server-side, secret key)
import { createClient } from '@vevee/sdk';

const vevee = createClient({ apiKey: process.env.VEVEE_SECRET_KEY! });

export async function GET(req: Request) {
  const userId = await currentUserId(req);

  const { output } = await vevee.compose<{
    title: string;
    body: string;
    ctaLabel: string;
    ctaHref: string;
  }>('onboarding-next-step', userId);

  return Response.json(output);
}

Same type, two very different users

Because every source resolves against the userId you pass, one compose type produces a different step per person. The teacher who imported a PDF and the student who pasted lecture notes hit the exact same call and the exact same prompt - but the assembled context differs, so the output differs. No persona switch, no second code path. You change the experience by improving the prompt in the dashboard, not by shipping a release.

  • Teacher (role=educator, event=imported_pdf) -> title: "Turn that PDF into a quiz", CTA: /quiz/new
  • Student (role=student, event=pasted_notes) -> title: "Summarize your lecture in 3 bullets", CTA: /summarize
  • Returning power user (high usage, many features) -> title: "Set up a shared workspace", CTA: /workspaces

Keep it off the hot path

Compose runs a real model call, so it is slower and costlier than a normal request - treat it that way. Generate the next step once, right after signup or on the first load of the onboarding screen, and cache the result on the user record. Re-generate only when their state meaningfully changes (they completed a step, they crossed a usage threshold), not on every render. The cost is metered against your workspace AI budget and returned on every call as usage.costMicroUsd, so you can watch exactly what onboarding personalization costs you per activated user.

Always have a fallback

Personalization should degrade gracefully. If compose throws ai_budget_exceeded because the budget is spent, or generation_failed on a transient model error, fall back to your old static first step rather than blocking the screen. The win is that the static checklist becomes the floor, not the ceiling: every user gets at least the generic path, and most get a step written for the job they actually showed up to do.

import { VeveeError } from '@vevee/sdk';

try {
  const { output } = await vevee.compose('onboarding-next-step', userId);
  return output;
} catch (e) {
  if (e instanceof VeveeError) return STATIC_FIRST_STEP; // never block onboarding
  throw e;
}

More from the blog

engineering · 5 min

How to reset usage limits when a subscription renews

A weekly plan, used twice. First week: ten generations. Second week: zero, because the counter never reset. This is the cron-job mistake - and the fix is one field on one call.

engineering · 6 min

How to manage subscription renewals: aligning Vevee with Stripe

A user signs up on Jan 15. Stripe charges them on the 15th of every month. Your metering layer resets on the 1st. Two clocks. One angry support ticket per cycle.

engineering · 5 min

Stop hardcoding your pricing page - render it from your metering layer

Every B2B SaaS I have shipped repeats the same mistake: plans live in two places - the dashboard that enforces them, and a const PLANS = [...] on the marketing site. They drift within a quarter.

thinking · 4 min

Meter AI by user, not by account - your margin depends on it

A few users will cost you 100x what your median user costs. If you only meter at the account level, you will not see them coming until your gross margin is gone.

engineering · 5 min

reserve / commit / release: the only correct way to enforce AI quotas

Every team I have seen build per-user AI metering has shipped a version of canUse → call OpenAI → track. It looks correct in single-threaded tests. It is broken in production.

thinking · 4 min

Why Stripe Billing is not enough for AI products

Stripe is excellent at one thing: turning usage into invoices. AI products need three other things, and Stripe does not do any of them.

thinking · 6 min

Paywall copy that rewrites itself for every user

Your paywall says "Upgrade to Pro for unlimited generations." A teacher reads it and shrugs. A student on a budget reads it and closes the tab. The same words, two lost conversions - because the words were written for nobody in particular.