skills/onboarding-flow/SKILL.md
--- name: onboarding-flow description: Scaffold a gamified onboarding checklist for a TanStack Start + Clerk + D1 app. Generates the D1 state model, server-event-driven completion handlers, a Home-page checklist component, and a PostHog activation funnel. Mirrors the "persistent dashboard widget + event-driven auto-tick" pattern documented in the llm-wiki canon. Use when starting a new app or when an existing app's empty Home dashboard needs to become an activation surface. category: development
npx skillsauth add RonanCodes/ronan-skills skills/onboarding-flowInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generates a working gamified-onboarding implementation for a TanStack Start + Clerk + D1 app. Output: a D1 migration, server-side event handlers, a React checklist component, and a PostHog funnel definition. Source: [[onboarding-checklist-ux]] in llm-wiki-research for the patterns this skill encodes.
onboarding_step table keyed (user_id, step_key) + Drizzle migration.GET /api/onboarding/state returns { steps, total, completed, percent } from D1.onboarding_step_completed with {user_id, org_id, step_key, sequence_number, time_since_signup_seconds}.onboardingCompleted on the Clerk user, written when the last step ticks. Lets edge auth check completion without a D1 round-trip.publicMetadata mirror for fast edge routing. localStorage is for UI niceties (collapsed/expanded), never for completion truth./onboarding-flow --steps connect-source,run-tool,send-chat,install-host my-app
/onboarding-flow --steps step-a,step-b,step-c,step-d --scaffold-route /home my-app
If --steps is omitted, the skill asks via AskUserQuestion for the activation moments.
Use AskUserQuestion to gather:
Confirm the resulting step list before scaffolding.
Write a Drizzle migration:
// drizzle/00NN_onboarding_step.sql (auto-generated)
CREATE TABLE onboarding_step (
id text PRIMARY KEY,
user_id text NOT NULL REFERENCES user(id) ON DELETE CASCADE,
step_key text NOT NULL,
completed_at integer NOT NULL,
payload text,
created_at integer NOT NULL DEFAULT (unixepoch())
);
CREATE UNIQUE INDEX onboarding_step_user_key_idx ON onboarding_step(user_id, step_key);
Plus the Drizzle TS:
export const onboardingStep = sqliteTable('onboarding_step', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
stepKey: text('step_key', { enum: STEP_KEYS }).notNull(),
completedAt: integer('completed_at', { mode: 'timestamp' }).notNull(),
payload: text('payload', { mode: 'json' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
}, (t) => ({
userKeyIdx: uniqueIndex('onboarding_step_user_key_idx').on(t.userId, t.stepKey),
}))
Mirrors ensureUserMirror and reconcileConnectionsForUser:
// src/lib/onboarding-mirror.ts
export async function markStepComplete(
db: AnySQLiteDb,
userId: string,
stepKey: StepKey,
payload?: Record<string, unknown>,
): Promise<{ alreadyComplete: boolean; sequence: number }> {
// SELECT existing; if found, return { alreadyComplete: true }
// Otherwise INSERT with completedAt = now, sequence = current count + 1
// Idempotent on (user_id, step_key)
}
Pure function, no cloudflare:workers imports, integration-testable with in-memory better-sqlite3 (the test helper from PR #12 in dataforce repo).
For each step in the user's list, identify the existing handler that signals completion and add a markStepComplete call inside the same transaction. Examples:
markStepComplete(db, userId, 'connect-shopify')POST /api/playground/run 200 path → markStepComplete(db, userId, 'run-tool')POST /api/chat first-stream-chunk → markStepComplete(db, userId, 'send-chat')tools/list arrival → detect host via User-Agent, markStepComplete(db, userId, 'install-<host>')If the action doesn't exist yet, the skill emits a TODO comment in the planned handler instead of a fake call.
Inside markStepComplete, after the insert succeeds, capture:
posthog.capture(userId, 'onboarding_step_completed', {
step_key: stepKey,
sequence_number: sequence,
time_since_signup_seconds: nowSeconds - userCreatedAtSeconds,
org_id: orgId,
})
Then write a saved insight in PostHog: a Funnel insight with steps in the order the user defined. The skill can't auto-create the insight, but emits a docs/posthog-setup.md note with the exact filter config.
// src/components/onboarding/checklist.tsx
'use client'
export function OnboardingChecklist({ steps, total, completed }: OnboardingState) {
const percent = Math.round((completed / total) * 100)
return (
<Card data-testid="onboarding-checklist" aria-label="Onboarding checklist">
<ProgressMeter value={percent} />
<ul role="list">
{steps.map((s) => <ChecklistItem key={s.key} step={s} />)}
</ul>
</Card>
)
}
Each ChecklistItem is a Card with title, description, primary CTA (<Link to={s.cta}>{s.ctaLabel}</Link>), status badge (<Badge variant={done ? 'default' : s.skipped ? 'secondary' : 'outline'}>), and a "Skip for now" link that hides the item from local React Query cache only.
Accessibility: every step has aria-label, the progress meter is role="progressbar", tab order matches visual order, and axe-core passes (verify in the e2e suite).
// src/routes/dashboard.tsx
function Home() {
const { data: state } = useQuery(['onboarding-state'], fetchOnboardingState)
if (!state) return <Loading />
if (state.completed === state.total) return <PopulatedDashboard celebrationBanner />
return <OnboardingChecklist {...state} />
}
Note: the populated dashboard (once it exists; in dataforce this is US-014 from v1) is a separate component. The skill doesn't generate it; it just wires the conditional.
The skill emits:
tests/integration/onboarding-state.test.ts — table shape, idempotent upsert, read-through.tests/integration/onboarding-events.test.ts — one test per event source.e2e/onboarding-home.spec.ts — incomplete state renders checklist, all-complete state renders populated dashboard, skip-for-now hides the step in-session, every CTA navigates.All using the existing in-memory better-sqlite3 helper.
pnpm test:integration passes.pnpm test:e2e passes (new auth-control + onboarding suites)./dashboard as a fresh user shows the checklist.pnpm quality green.markStepComplete into existing handlers.[[onboarding-checklist-ux]] — the canon page this skill encodes (research, patterns, anti-patterns)[[ideal-tech-setup]] § Greenfield Spec Baseline — onboarding belongs as a first-class story in any greenfield spec/generate-spec — emits an onboarding US-* automatically when scaffolding a new web app/ralph — runs the resulting PRDdevelopment
Close the loop on a Linear ticket when its work ships - move the status and post a deploy comment with the PR link, what shipped, and a try-it link, mentioning the collaborator. Used as the tail of /ro:linear-nightshift for every merged mirror, or manually after an ad-hoc build. Triggers on "linear update", "update the linear ticket", "mark NUT-x done", "tell eoin it shipped", "/ro:linear-update".
devops
Run a night-shift against a collaborator's Linear board. Pulls the team's Grilled tickets (/ro:linear-grill moves a ticket to Grilled once its questions are answered), VERIFIES the questions were actually answered (unanswered → bounce the ticket to the "Question for <name>" state), mirrors verified tickets to ephemeral GitHub issues with ready-for-agent, then runs the standard /ro:night-shift machinery on GitHub. Tail-calls /ro:linear-update for everything that merged + deployed. Triggers on "linear nightshift", "nightshift linear", "drain the linear board", "run the shift off linear", "/ro:linear-nightshift".
development
Grill a collaborator's Linear tickets and move every processed ticket to where it belongs. Resolves the board from the repo's .ro-linear.json, reads the collaborator's Backlog / Ready-for-agent issues, then per ticket either posts 3-5 decision-extracting questions (state moves to "Question for <name>") or confirms it build-ready (state moves to "Grilled", the gate /ro:linear-nightshift consumes); shipped-and-confirmed tickets close as Done. The async-collaborator counterpart of /ro:day-shift for people who never touch GitHub. Triggers on "grill linear", "grill eoin's tickets", "linear grill", "add questions to the linear tickets", "/ro:linear-grill".
development
--- name: about-page description: Add a standard About page to any web app, what it is, the tech stack, and an FAQ, wired into a footer link with a sticky footer. Built with Spartan + Tailwind (the canonical component layer) and falls back to semantic HTML so it ships reliably. Use whenever building, polishing, or shipping an app, every app should have one. Triggers on "add an about page", "about page", "footer about link", or as a standard step in app build/polish. category: frontend argument-h