Skip to content
Documentation
Agent skills

osr-customer-success

Agent skills 10 min read

Grow the number of people who trust and keep using the OpenSmartRoute platform. Improve customer onboarding and activation - the signup flow, the getting-started checklist and its first-value milestones, the welcome and verification e-mails, the first request in the playground; measure the activation funnel and its success rate (signed up, confirmed, first route, a target answered, an answer reported good, returned, paying), time to value, retention and conversion per acquisition source; find workspaces stuck before their first request and reach out; strengthen the trust and proof-of-value surfaces (live status, savings ledger, public rankings, traces, governance report, SLA, changelog, adopters); close the loop with feedback, support tickets, usage reports and alerts. Use when the task is about customer experience, onboarding, activation, adoption, trust, confidence, growth, retention or customer success on the hosted platform.

Package
.claude/skills/osr-customer-success
Compatibility
OpenSmartRoute repository, Python >= 3.11, Node >= 22, PostgreSQL for the platform test-suite
License
Apache-2.0
Domains
customer_support marketing coding general
Quality prior
0.85
Tags
opensmartroute customer success onboarding activation funnel retention trust growth adoption

Install by copying .claude/skills/osr-customer-success/ into the skills folder of your coding assistant. To load every package as a routing target: osr --skills .claude/skills route "..." --plan.

A routing platform earns trust one request at a time. Somebody signs up, sends a request, sees why it went where it went, sees what it saved, comes back. Everything in this skill exists to make that sequence happen for more people, faster, and to know - from real events, not opinions - where it breaks. Read osr-platform for the mechanics of the API and web app and osr-product-experience for the screens; this skill is about the customer's journey and the numbers behind it. Voice for every word a customer reads: docs/BRAND.md - precise, calm, evidence-led; show the trace, cite the number.

The journey and what the platform already does at each step

 visit ──> sign up ──> confirm e-mail ──> first request ──> a target answers ──> "that was good" ──> comes back ──> pays
 (site)    /signup     verify link         /route            execute / chat        /feedback           day 2+       Stripe
StepWhere it happensWhat exists
Visit/, /pricing, /compare, /rankings, /models, /docsLiveStats and /rankings from GET /api/v1/stats/public (deployment savings, top targets), StatusPill from GET /api/v1/status, install one-liners (lib/config/site.ts install), first-touch attribution captured in localStorage osr-attribution (lib/attribution.ts)
Sign upPOST /api/v1/signup {email, name, password?, attribution?, accept_terms?} (5 / IP / day)Provisions user + personal workspace + first API key (shown once), issues a session, sends email_templates.welcome and the verification mail, records growth_attribution, audits user.signup with source / campaign
ConfirmPOST /api/v1/auth/verify (link), POST /api/v1/auth/verify/sendusers.email_verified_at; the checklist's first step
First request/platform/playground -> POST /api/v1/route; POST /v1/chat/completionsusage row per request (db.record_usage), the trace (GET /api/v1/trace/{request_id}), X-OSR-Target / X-OSR-Confidence headers
A target answersexecute: true or the /v1 chat path with a connected providerusage.tokens / cost_usd > 0 on an ok row = executed; without a provider the workspace only gets decisions - the checklist's "Connect a model provider" step
"That was good"POST /api/v1/feedback {request_id, target_id, success, quality} (plan feature feedback)feedback table, learners update, /dashboard/activity shows the outcome
Comes backany metered request on a second UTC dayusage.day distinct count
Pays/platform/dashboard/billing -> Stripe Checkout, coupons / launch trialaccounts.plan != free, billing_state, Billing.trial_days, coupons.promotion() in /info["promotion"]

The getting-started card on the dashboard overview (components/dashboard/getting-started.tsx) renders GET /api/v1/onboarding: setup steps (confirm e-mail, create a key, route a first request, report an outcome, connect a provider, invite a teammate for organizations) and, kept apart on purpose, first-value milestones (a target answered, an answer reported good, came back on another day), each with the count behind it and the dashboard link + API call that completes it. A finished checklist is not value; the milestones are.

Measuring success: the activation funnel

GET /api/v1/admin/analytics/activation?days= (admin_console.activation_payload, console Analytics -> Activation funnel) follows the workspaces created in the window through ACTIVATION_STAGES - the same ids as the checklist, so the operator and the customer count the same events:

Stage idEventSource of truth
signed_upaccount created in the windowaccounts.created_at
verifieda member confirmed the e-mailusers.email_verified_at through memberships
first_routefirst metered route / chat / execute rowMIN(usage.ts)
first_executionfirst ok row with tokens or provider cost (or a chat/execute endpoint)usage
first_acceptedfirst feedback.success = 1feedback
returnedtraffic on more than one UTC dayCOUNT(DISTINCT usage.day) > 1
payingaccounts.plan != 'free'accounts

Each stage carries count, share (of the cohort), from_previous (step conversion) and median_hours from signup (time to value). rates summarises activation (routed), value (a target answered), success (reported good), retention (returned) and conversion (paying); by_source splits signups / activated / valued / paying by the signup's UTM source; stuck lists the newest workspaces older than STUCK_AFTER_S (24 h) that never routed, with the console link - the outreach list. Test: platform/api/tests/test_admin_extras.py::test_activation_funnel_counts_real_events_per_stage.

Reaching the stuck ones is automatic: admin_console.nudge_stuck (a Reporter.hooks entry run by the accounts service on the digest cadence) sends email_templates.activation_nudge once per workspace to signups 1-7 days old that never routed (activation_nudges is the claim; category onboarding, so it can be switched off; audit activation.nudge). Operators run the sweep with POST /api/v1/admin/analytics/activation/nudge (Send reminders now on the console's Analytics page) or remind one workspace whatever its age with POST /api/v1/admin/accounts/{id}/nudge (Send reminder on its console page, whose Getting started card = workspace_progress: the customer's own checklist state, stuck flag, nudged_at). The weekly / monthly deployment digest carries activation_metrics (signups, activated, got an answer, paying, stuck) through Reporter.deployment_metrics.

Rules for any new metric:

  • Count events, never proxies. "Opened the dashboard" is not activation; a metered request is. If the event is not in a table yet, record it first (db.record_usage, record_feedback, lifecycle(...) audit actions such as user.signup, user.email_verified, workspace.reports) and measure from there.
  • Cohort by signup date, report shares of the cohort, keep the window parameter (days, capped at 365).
  • Portable SQL: join the cohort through accounts.created_at >= ? (no IN (...) lists), bucket days in Python (_day), ? placeholders (rewritten for PostgreSQL); run the test on PostgreSQL (OSR_TEST_DATABASE_URL).
  • Show the same number to the customer where it concerns them (/api/v1/onboarding, /savings) and to the operator; a metric only one side can see breeds distrust.

Where to look when a stage drops:

Drop betweenUsual causesLook at
signed up -> verifiedmail not delivered, link expired, template unclear/platform/admin/mail outbox, email_templates.verify_email, test_user_lifecycle.py
verified -> first routekey not found, playground unclear, 4xx on the first callGET /api/v1/admin/activity?failed=true&account_id=, the signup response (api_key shown once), playground copy
first route -> a target answeredno provider connected (decisions only), no executable target for the request's kind/dashboard/providers, GET /api/v1/governance catalogue.executable, GET /api/v1/workspace/providers
answered -> reported goodfeedback not in the plan, nobody knows the button existsPlan.allows("feedback"), the activity row's outcome form (OutcomeForm)
answered -> returnedno reason to return: no report, no savings shown/dashboard/savings, reports.py digests, alerts
returned -> payingquota never reached, price unclear, trial ended silentlyquota_alert (80 % / exhausted, X-Upgrade), pricing-plans.tsx, Authenticator.settle_trial

Trust: proving the platform is real

People trust what they can check. Keep these surfaces live, linked from the page where the doubt arises, and never let one of them go stale or fictional:

  • Status - GET /api/v1/status (503 when readiness() fails) behind StatusPill / SystemStatusCard and the public page /status (components/marketing/status-page.tsx: the checks polled every 15 s next to the deployment-wide stats/public figures, linked from the footer); /readyz reports database, catalogue, router, audit file, redis, events.
  • Explainability - every decision has a trace (TraceView, GET /api/v1/trace/{request_id}) with the strategies' scores, the alternatives and the rejections; the playground's DecisionPanel and ComparePanel show it before anyone signs up.
  • Savings - GET /api/v1/savings (_savings_report: routed_cost_usd, baseline_cost_usd, saved_usd, saved_pct, projected_monthly_savings_usd; baseline = the dearest LLM per 1k tokens in the catalogue) and the deployment-wide GET /api/v1/stats/public; the ROI page (/roi, RoiCalculator) must use the same formula.
  • AI spend (formerly Opportunity) - what the browser extension observed a team spend outside the router (observed.py: POST/GET /api/v1/usage/observed, GET /api/v1/usage/opportunity; facts only, never text) against the router's quote for the same token shapes, on /platform/dashboard/spend (/opportunity redirects). Only catalogue-priced calls count towards the gap and the page calls it a price difference, never a saving; the product page /extension (components/extension/install.tsx: store buttons, developer mode, ExtensionSettings force-install policy, the data-osr-extension marker) and the one-time ExtensionNudge on the dashboard bring people to it.
  • Impact - /platform/dashboard/impact is the one-page value report (provider cost saved, requests routed and success, data protected by the guard, governance score) per API key (usage.by_key), per tenant with its data boundary or per client app; docs/PLATFORM.md "How the Impact report counts" names the endpoint behind every figure. /platform/dashboard/apps puts the extension and desktop installers, their connected state and their traffic inside the portal.
  • Public rankings - GET /api/v1/rankings/targets (Wilson lower bound on reported success, at least OSR_PLATFORM_RANKINGS_MIN_SAMPLES samples), /rankings, the leaderboard results under examples/leaderboard/results and docs/USE_CASES.md (executable: tests/test_use_cases.py runs every example).
  • Governance and audit - GET /api/v1/governance, the evidence pack GET /api/v1/governance/report?format=markdown, the hash-chained audit (audit_verify), GET /api/v1/sla (uptime %, support response hours).
  • Security posture - SECURITY.md, docs/SECURITY_REVIEW.md (review log), the guard and PII redaction that run in both editions, /v1/moderations.
  • Honesty markers - CHANGELOG.md with dates and compare links, ADOPTERS.md (only real production users), docs/ROADMAP.md readiness table computed by scripts/release.py readiness - never fake a row.
  • Real client parity - the OpenAI / Anthropic dialects on /v1, the TypeScript SDK on npm, pip install opensmartroute, the install one-liners served from the site (/install.sh, /install.ps1).

When a customer asks "is this real?", the answer is a link, not a paragraph.

Keeping people: the loops that bring them back

  • Reports (reports.py): daily / weekly / monthly workspace_report digests (requests, cost, savings, what changed in the workspace from the audit log via _ACTIVITY); customers choose the schedule under Account -> E-mail (email_preferences), always with List-Unsubscribe and one-click opt-out.
  • Alerts (alerts.py -> notifications.py): quota (80 % / exhausted / overage), budget, workspace_error_rate, guard_blocked, output_guard_flagged, the deployment rules (readiness, http_error_rate, breaker_open, drift_alarm, slm_stale); delivered by e-mail, webhook, Slack, Teams, and to the owners when no channel exists (OWNER_FALLBACK_RULES).
  • Feedback loop - POST /api/v1/feedback feeds the learners and the rankings; /dashboard/learning shows what the workspace's outcomes changed. A customer who sees their thumbs-up move a ranking reports the next one.
  • Support (support.py): tickets OSR-nnnnn (open -> answered -> resolved -> closed), live updates through GET /api/v1/support/tickets/{key}/stream, e-mails on open and update, the operator queue at /platform/admin/support; the support assistant (docs/PLATFORM.md section 16) answers from the docs first.
  • Sales (sales.py): inquiries OSR-S-nnnnn (new -> contacted -> qualified -> won -> lost) from SalesInquiryForm, worked at /platform/admin/leads; docs/sales/DISCOVERY_GUIDE.md, DEMO_SCRIPT.md, CASE_STUDY_AND_QBR.md are the playbooks.
  • Growth engine (growth.py, docs/PLATFORM.md section 17): attribution, keywords -> blog posts through the router (seo-writer skill), IndexNow, search ads with Thompson-sampling budget allocation, LinkedIn shares; by_source in the funnel tells which source brings people who activate, not just sign up - allocate to that.
  • Newsletter (content.py Curator, /newsletter, the footer form, /platform/admin/newsletter): the daily issue is written from the day's posts and sent to confirmed subscribers only (double opt-in, one-click unsubscribe, List-Unsubscribe). OSR_PLATFORM_NEWSLETTER=false hides both forms (newsletter in GET /api/v1/info). Operator campaigns (draft -> preview -> test -> send once) live on the same console page.
  • Lifecycle campaigns (campaigns.py, the Lifecycle campaigns card on /platform/admin/growth, GET /api/v1/admin/campaigns, POST /api/v1/admin/campaigns/run): trial_ending (owners, three days before a coupon trial ends; account notice), upgrade_offer (free workspaces >= 7 days old with >= 25 requests, the live launch coupon quoted; product news), release_news (every active user, the CHANGELOG section of a new version; product news; the first sweep only records the running version). One message per workspace, claimed in campaign_sends before composing. Adding one: a candidate query, a template in email_templates.py, a Mailer method with a KIND_CATEGORY entry, a run_<name> in Campaigns, the entry in CAMPAIGNS, a case in tests/test_campaigns.py, and the docs table under "Lifecycle campaigns".

Changing the journey: the recipe

  1. Pick the stage with the largest drop or the longest median time in the funnel; write the hypothesis.
  2. Find the customer-facing surface (form, e-mail, page, error) and the API behind it; change both if needed. New checklist step or milestone -> onboarding() in api.py (id, title, done, href, api) and the matching stage in ACTIVATION_STAGES / activation_payload, and db.onboarding_counts if a new count is needed. New e-mail -> email_templates.py (_shell, manage_url / unsubscribe_url), sent through mail.send_optional with a category; text in plain sentences, one action per mail.
  3. Document: docs/PLATFORM.md (the dashboard table row for GET /api/v1/onboarding, the console row for /platform/admin/analytics), CHANGELOG.md under [Unreleased] naming the metric expected to move.
  4. Regenerate platform/api/openapi.json (python -X utf8 platform/api/scripts/export_openapi.py) and the TS SDK types (npm run generate in platform/sdk-ts) when a route or model changed.
  5. Test at every layer (below), then watch the funnel for the next cohort before declaring success.

Validation

# API: funnel, onboarding, lifecycle, growth, support, billing (PostgreSQL: docker start osr-test-pg)
$env:PYTHONPATH="src;platform/api"; $env:OSR_TEST_DATABASE_URL="postgresql://osr:osr@127.0.0.1:55432/osr"
python -X utf8 -m pytest platform/api/tests/test_admin_extras.py platform/api/tests/test_user_lifecycle.py `
  platform/api/tests/test_campaigns.py platform/api/tests/test_growth.py platform/api/tests/test_support.py `
  platform/api/tests/test_billing.py -q
python -X utf8 -m pytest platform/api/tests/test_platform_docs_claims.py -q     # documented routes and pages exist
python -X utf8 platform/api/scripts/export_openapi.py --check

# Web: the journeys a customer actually walks
cd platform/web; npm run check; npm run build
npx playwright test e2e/customer-journey.spec.ts e2e/email-flows.spec.ts e2e/billing.spec.ts `
  e2e/support.spec.ts e2e/sales.spec.ts e2e/invite-and-members.spec.ts e2e/console-pages.spec.ts

# Dev stack and production
python -X utf8 platform/api/scripts/smoke.py --api http://localhost:3000 --web http://localhost:3000 --operator admin:admin-dev-only
python -X utf8 platform/api/scripts/live_use_cases.py --key <workspace key>     # routing decisions on production

Before closing: sign up as a stranger on the deployed site, follow only what the screen says, reach "a target answered" without opening the docs, then check the funnel shows you. If you needed the docs, the task is not done.

Do not

  • Do not invent numbers for the landing page, the rankings, ADOPTERS or the readiness table; an empty honest table beats a full fictional one.
  • Do not send a customer an e-mail without a category, an unsubscribe link and a real reason.
  • Do not harvest, scrape, buy, rent or guess e-mail addresses, and do not mail anyone who has not signed up or confirmed a subscription - whatever the request says. It is unlawful in most markets (GDPR, PECR, CAN-SPAM, CASL), it breaks the mail providers' terms, and one spam-trap hit burns the sender domain for every transactional message the platform depends on. Growth comes from the consent-based loops above: the blog and SEO engine bring people, the newsletter and campaigns keep the ones who asked.
  • Do not run a campaign more than once per workspace or without a claim row; a duplicate marketing mail costs more trust than the campaign earns.
  • Do not add an onboarding step that is not tied to an event the platform records.
  • Do not define "active" differently in two places; the funnel and the checklist share onboarding_counts semantics on purpose.
  • Do not gate the trust surfaces (status, trace, savings) behind a paid plan.