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 PRDtesting
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".