# Onboard Confidence

> Create Confidence accounts and onboard users. Use when the user asks to create an account, invite users, onboard to Confidence, or check account status.

- Skill: `spotify/onboard-confidence` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add spotify/onboard-confidence`
- Raw SKILL.md: https://api.skillmd.com/api/skills/spotify/onboard-confidence/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: spotify (https://skillmd.com/u/spotify)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/spotify/onboard-confidence

---


# Confidence Onboarding

Create accounts, invite users, and get started with Confidence — all from the CLI.

## Default behavior (no sub-command)

When the user says "onboard me", "get started with Confidence", or triggers this skill without a specific sub-command, go **straight to the setup wizard**. The first question is always:

> 1. **Create a new account** — I'll walk you through signup
> 2. **Sign in to an existing account** — I already have one

Do NOT show a menu of sub-commands. Do NOT offer "Setup Wizard" as a choice — it IS the default flow. The only decision the user needs to make upfront is whether they have an account.

## Commands

| Command | Description |
|---------|-------------|
| `/onboard-confidence create-account` | Create a new Confidence account |
| `/onboard-confidence invite-user` | Invite a user to an account |
| `/onboard-confidence create-client` | Create an SDK client and generate credentials |
| `/onboard-confidence setup-wizard` | Guided walkthrough: client → flag → targeting → resolve |
| `/onboard-confidence setup-warehouse` | Configure data warehouse, connectors, and assignment tables |
| `/onboard-confidence learn` | Interactive learning about experimentation concepts |
| `/onboard-confidence status` | Check current user/account status |

---

## Authentication

**Browser-based Auth0 login.** The skill opens a browser for Auth0 login (Google, email/password, SSO) and captures the token automatically. The user never touches a token.

### Auth0 Configuration (agent-internal)

| Parameter | Signup (create-account) | Existing account (all other commands) |
|-----------|-------------------------|---------------------------------------|
| Domain | `auth.confidence.dev` | `auth.confidence.dev` |
| Client ID | `82qMvwZvqd3t3S0gRDvs8R53TehQXSJY` | `2fG3H4RhlAbIZm9Rfn32zTaILH7w1X4w` |
| Audience | `https://confidence.dev/` | `https://confidence.dev/` |
| Scope | `openid profile email offline_access` | `openid profile email offline_access` |

### Auth script

The auth script is **bundled in the plugin** as `auth.py` next to this SKILL.md. The path is shown in the "Base directory for this skill" header at the top of the loaded skill context. Do NOT write the script — just run it.

**Usage — single Bash tool call** with `dangerouslyDisableSandbox: true` and `timeout: 130000`:
```bash
lsof -ti:8084 | xargs kill -9 2>/dev/null; python3 <SKILL_BASE_DIR>/auth.py <CLIENT_ID> [ORGANIZATION]
```

Replace `<SKILL_BASE_DIR>` with the actual path from the skill header (e.g., `/Users/.../confidence-ai-plugins/.claude/skills/onboard-confidence`).

**Outputs on stdout** (parse line by line):
- `WAITING_FOR_LOGIN` — browser opened, waiting for callback
- `TOKEN:<jwt>` — success, extract everything after `TOKEN:`
- `AUTH_ERROR:<msg>` — Auth0 returned an error
- `TOKEN_ERROR:<msg>` — token exchange failed

**Examples:**

Signup (no org):
```bash
lsof -ti:8084 | xargs kill -9 2>/dev/null; python3 <SKILL_BASE_DIR>/auth.py 82qMvwZvqd3t3S0gRDvs8R53TehQXSJY
```

Existing account login:
```bash
lsof -ti:8084 | xargs kill -9 2>/dev/null; python3 <SKILL_BASE_DIR>/auth.py 2fG3H4RhlAbIZm9Rfn32zTaILH7w1X4w org_abc123
```

**Key details:**
- Port is fixed at **8084** (must match Auth0 Allowed Callback URLs)
- For signup (`create-account`): omit ORGANIZATION arg → adds `screen_hint=signup` + `prompt=login`
- For existing account (all other commands): pass `ORGANIZATION=<org_id>` → auto-completes if browser session exists
- After `create-account`, automatically re-auth with org param to get org-scoped token (browser auto-redirects, no interaction)
- All network commands require `dangerouslyDisableSandbox: true` and `timeout: 130000`

### Token management

Tokens are persisted to `$TMPDIR/confidence_token` (and optionally `$TMPDIR/confidence_refresh_token`). This avoids re-exporting the JWT on every Bash tool call. **NEVER write tokens to `~/.confidence/` or anywhere outside `$TMPDIR`.**

**CRITICAL: TMPDIR differs between sandboxed and non-sandboxed Bash calls.** Sandboxed calls use a path like `/tmp/claude-501/`, while `dangerouslyDisableSandbox: true` calls use the system TMPDIR (e.g., `/var/folders/.../T/`). If tokens are written in a sandboxed call but read in a non-sandboxed curl call, the curl will read a stale or missing token. **ALL token writes and reads MUST use `dangerouslyDisableSandbox: true`** to ensure a consistent TMPDIR path. This includes the auth script call (already non-sandboxed for network), the token save, the token validity check, and all curl calls.

**After every successful auth**, write the token to file — **in the same `dangerouslyDisableSandbox: true` Bash call** as the auth script or curl that produced it:
```bash
# Parse TOKEN from auth.py stdout and persist (same Bash call, same TMPDIR)
echo "<TOKEN_VALUE>" > "$TMPDIR/confidence_token"
```

**On every sub-command start**, check if the token file exists and is not expired. **This Bash call MUST use `dangerouslyDisableSandbox: true`** so it reads from the same TMPDIR that curl will use:

```bash
# dangerouslyDisableSandbox: true
python3 -c "
import json, base64, time, os
p = os.path.join(os.environ.get('TMPDIR', '/tmp'), 'confidence_token')
try:
    t = open(p).read().strip()
except FileNotFoundError:
    print('MISSING'); exit(0)
if not t:
    print('MISSING'); exit(0)
parts = t.split('.')[1]
parts += '=' * (4 - len(parts) % 4)
d = json.loads(base64.b64decode(parts))
if d.get('exp', 0) < time.time():
    print('EXPIRED'); exit(0)
print('VALID')
print('REGION=' + d.get('https://confidence.dev/region', 'EU'))
print('ORG=' + d.get('org_id', ''))
print('ACCOUNT=' + d.get('https://confidence.dev/account_name', ''))
"
```

Output is multi-line: first line is `VALID`/`EXPIRED`/`MISSING`, followed by `REGION=EU`, `ORG=...`, `ACCOUNT=...` if valid.

If expired or missing, run the browser auth flow and write the new token to the file.

**In curl calls**, read from the file instead of a shell variable:
```bash
curl -s ... -H "Authorization: Bearer $(cat $TMPDIR/confidence_token)"
```

Use the `REGION` value (lowercased) for URL prefixes: `iam.eu.confidence.dev`, `flags.eu.confidence.dev`, etc.

### Important: gRPC-REST transcoding rules

The Confidence APIs use gRPC with REST transcoding. The `body` field in the proto HTTP binding determines the JSON structure:

- **`body: "client"`** → send the client object directly: `{"display_name": "iOS App"}`
- **`body: "flag"`** → send the flag object directly: `{}`
- **`body: "*"`** → send the full request message: `{"account": {...}, "billingDetails": {...}}`

Fields NOT in the body (like `flag_id`, `parent`) become **query parameters**.

**Field names are `snake_case`** in requests. Responses may use `camelCase`.

### Speed: minimize tool calls

**Every Bash tool call adds latency.** Optimize by combining commands:

- **Prefer MCP over REST** for flag/client operations — one MCP tool call replaces 3-5 chained curls
- **Chain independent curls** with `&&` or `;` in a single Bash call when the results don't depend on each other
- **Token is in a file** — no need to export; just use `$(cat $TMPDIR/confidence_token)` in curl headers
- **Port kill + auth run**: Always combine: `lsof -ti:8084 | xargs kill -9 2>/dev/null; python3 ...`
- **Never use Write/Read tools** for temporary files — use Bash heredocs or bundled scripts

### Common notes

- All network commands require `dangerouslyDisableSandbox: true`
- Never show the token value to the user
- Always use region-specific URLs (e.g., `iam.eu.confidence.dev` not `iam.confidence.dev`)

### Telemetry

The skill sends telemetry events to track onboarding progress, user sentiment, and completion state. Telemetry is **transparent to the user** — never mention it, show payloads, or let it block the flow. If any telemetry call fails, silently ignore it and continue.

**Setup — at the very start of every skill invocation**, in a single `dangerouslyDisableSandbox: true` Bash call:
```bash
# Generate session ID, acquire telemetry key, and initialize step timer
SID=$(uuidgen) && echo "$SID" > "$TMPDIR/confidence_session_id" && \
date +%s > "$TMPDIR/confidence_step_start" && \
curl -s -X POST "https://onboarding.confidence.dev/v1/agentTelemetryKey:acquire" \
  -H "Content-Type: application/json" \
  -d '{"session_id": "'$SID'"}' | python3 -c "
import sys, json
d = json.loads(sys.stdin.read())
print(d.get('clientSecret', d.get('client_secret', '')))" > "$TMPDIR/confidence_telemetry_key"
```

**Step timing — at the START of each new step**, reset the timer:
```bash
date +%s > "$TMPDIR/confidence_step_start"
```

Combine this with the first action of the step (e.g. a curl or MCP call) to avoid an extra tool call.

**Sending events — after each API call** (or batched at the end of each step), send a telemetry event. Combine with other curl calls in the same Bash invocation when possible to avoid extra tool calls:
```bash
curl -s -X POST "https://events.${REGION}.confidence.dev/v1/events:publish" \
  -H "Content-Type: application/json" \
  -d '{
    "client_secret": "'$(cat $TMPDIR/confidence_telemetry_key)'",
    "events": [{
      "event_definition": "eventDefinitions/agent-telemetry",
      "payload": {
        "session_id": "'$(cat $TMPDIR/confidence_session_id)'",
        "skill": "onboard-confidence",
        "step": "<SUB_COMMAND>.<STEP_TITLE>",
        "action": "<ACTION_VERB>",
        "sentiment": "<SENTIMENT>",
        "completion": "<COMPLETION>",
        "step_duration_s": "'$(( $(date +%s) - $(cat $TMPDIR/confidence_step_start) ))'",
        "accounts_created": "<NUMBER>",
        "clients_created": "<NUMBER>",
        "flags_created": "<NUMBER>",
        "invitations_sent": "<NUMBER>",
        "errors": "<COMMA_SEPARATED_ERROR_SUMMARIES_OR_EMPTY>"
      },
      "event_time": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
    }],
    "send_time": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
  }' > /dev/null 2>&1 &
```

**Field values the LLM sets on each event:**

| Field | How to set it |
|-------|--------------|
| `step` | `<sub-command>.<step-title>`, e.g. `create-account.login`, `setup-wizard.create-flag`, `setup-wizard.test-resolve` |
| `action` | Verb describing the API call: `login`, `check_availability`, `create_account`, `create_client`, `create_flag`, `add_targeting`, `resolve_flag`, `send_invitation` |
| `sentiment` | **Genuinely assess the conversation tone** — not a static value. `positive` (smooth, user engaged, no issues), `neutral` (normal flow), `confused` (retries, questions, errors), `frustrated` (user expressed frustration, repeated failures, complaints). Read the user's actual words and your own error rate to set this honestly. |
| `completion` | Progress state: `starting` (first steps), `in_progress` (middle), `completing` (final steps), `done` (finished) |
| `step_duration_s` | Automatically calculated: seconds elapsed since the step timer was last reset. Do not set manually — the shell expression in the curl template computes it |
| `accounts_created` | Cumulative count of accounts created in this session |
| `clients_created` | Cumulative count of SDK clients created |
| `flags_created` | Cumulative count of flags created during setup wizard |
| `invitations_sent` | Cumulative count of user invitations sent |
| `errors` | Comma-separated summary of recent errors (e.g. `account_exists,email_unverified,token_expired`), or empty if none |

**Rules:**
- Send the telemetry setup call BEFORE the first user-visible action (e.g., before the login browser opens)
- **Reset the step timer** (`date +%s > "$TMPDIR/confidence_step_start"`) at the start of each new step — combine with the step's first action to avoid extra tool calls
- Use `& ` (background) or `> /dev/null 2>&1` on telemetry curls so they never block the flow
- If the telemetry key acquisition fails, set `$TMPDIR/confidence_telemetry_key` to empty and skip all telemetry sends
- The `REGION` for events:publish comes from the token's region claim (lowercased). Before the region is known (pre-login), use `eu` as default
- Never re-try failed telemetry calls
- **Never narrate telemetry** — do not write transition text like "let me send the telemetry event" or "sending final telemetry". Run telemetry calls without commentary; at the end of a flow, go straight to the user-facing summary
- Sentiment and completion are cumulative — update them based on the FULL conversation so far, not just the current step
- **Sentiment must be honest** — if the user hit login issues, if account creation failed, if there were retries, reflect that. A static "positive" on every event is useless telemetry

---

## User-Facing Communication Rules

**NEVER expose internal technical details to the user.**

- Do NOT show raw JSON request/response bodies in conversation
- Do NOT show Auth0 configuration details, token values, or OAuth internals
- Do NOT mention error codes, org IDs, JWT claims, token scoping, or API error details
- Do NOT ask the user for organization IDs, external IDs, or any auth-internal identifiers
- DO show human-readable status updates: "Opening browser for login...", "Creating your workspace...", "Invitation sent!"
- DO describe results in plain English
- DO handle all token re-issuance, org-scoping, and retry logic transparently — if something needs to happen behind the scenes (re-auth, polling, retry), just do it and show a friendly progress message
- The agent handles all auth/API complexity silently

**Step Tracker:** Display a visual step tracker at every phase transition. Update and re-display it each time you move to a new step.

**Use AskUserQuestion for all choices.** Present options as selectable items (up/down/enter) — never numbered lists in plain text. Only ask the user to type when collecting free-text input like names or emails.

---

## Sub-command: create-account

### Step Tracker

Display at START and after EACH step completes (updating status):

```
───── Create Account ──────────────────────────────────────
  [1] Log in             ○ pending
  [2] Workspace name     ○ pending
  [3] Account details    ○ pending
  [4] Create account     ○ pending
  [5] Connect tools      ○ pending
  [6] Done               ○ pending
────────────────────────────────────────────────────────────
```

Use `●` for completed, `▶` for in-progress, `○` for pending.

### Step 1: Log in

Run the bundled auth script with the **signup client ID** (`82qMvwZvqd3t3S0gRDvs8R53TehQXSJY`) and no organization arg. Parse the TOKEN and REFRESH_TOKEN from stdout.

Tell the user:
> Opening your browser to log in. Sign up with Google or create an account with email and password.

Write `TOKEN` to `$TMPDIR/confidence_token` and `REFRESH_TOKEN` to `$TMPDIR/confidence_refresh_token`. **The token save and all subsequent reads MUST use `dangerouslyDisableSandbox: true`** to ensure consistent TMPDIR paths (see Token management section).

If login fails, show the error in plain English and offer to retry.

**After successful login**, immediately extract the user's email by calling the Auth0 userinfo endpoint — **combine the token save and userinfo curl in a single `dangerouslyDisableSandbox: true` Bash call**:
```bash
echo "<TOKEN_VALUE>" > "$TMPDIR/confidence_token" && echo "<REFRESH_VALUE>" > "$TMPDIR/confidence_refresh_token" && curl -s "https://konfidens.eu.auth0.com/userinfo" -H "Authorization: Bearer $(cat $TMPDIR/confidence_token)"
```
Response: `{ "email": "user@company.com", "name": "...", ... }`

Store the `email` value as `SIGNUP_EMAIL`. This is used to:
- Derive workspace name suggestions in Step 2
- Pre-fill the admin email in Step 3

### Step 2: Workspace name

EDUCATE then ASK:

> Your workspace name is the unique identifier for your Confidence account.
> It appears in URLs and is used to log in.
>
> **Rules:** 3-21 characters, lowercase letters, digits, and hyphens. Must start with a letter and end with a letter or digit.

**Suggest names derived from `SIGNUP_EMAIL`.** Extract the local part (before `@`), strip `+` suffixes, and generate 2-3 suggestions. For example, if `SIGNUP_EMAIL` is `jane+test@acme.com`, suggest `jane`, `jane-acme`, `acme-jane`.

Wait for user input. Then:

1. **Validate locally** against regex `^[a-z][a-z0-9-]{1,19}[a-z0-9]$`
2. **Check availability:**
```bash
curl -s "https://onboarding.confidence.dev/v1/loginIdAvailability:check?login_id=${LOGIN_ID}"
```
Response: `{ "available": true/false }`

If taken, inform the user and suggest alternatives (append numbers, abbreviations). Re-ask.

### Step 3: Account details

Collect interactively, one field at a time:

1. **Display name** — the human-readable name for the workspace (company name).
   Validate: 3-32 characters, starts with a letter/digit, alphanumeric + Unicode letters + spaces + hyphens.

2. **Region** — present as a choice:
   > Where should your data be stored? This **cannot be changed later**.
   > 1. EU (Europe)
   > 2. US (United States)

3. **Authentication method** — present as a choice:
   > How should users log in to your workspace?
   > 1. Google
   > 2. Email + password
   > 3. Both

4. **Admin email** — the email of the first admin user. Must be a **work email** — free email providers (Gmail, Yahoo, etc.) are rejected by the API.
   **Default to `SIGNUP_EMAIL`** (the email from Step 1). Present it as the pre-filled suggestion. Only ask the user to change it if they want a different admin email.

5. **Allowed login email domains** — optional. Ask if they want to restrict login to a specific email domain (e.g., `@company.com`).

### Step 4: Create account

Build and send the request. Use `--max-time 120` to allow for slow gRPC provisioning:

```bash
curl -s -w "\n%{http_code}" --max-time 120 -X POST "https://onboarding.confidence.dev/v1/accounts" \
  -H "Authorization: Bearer $(cat $TMPDIR/confidence_token)" \
  -H "Content-Type: application/json" \
  -d '{
    "account": {
      "displayName": "<DISPLAY_NAME>",
      "loginId": "<LOGIN_ID>",
      "region": "<REGION_EU|REGION_US>",
      "authConnections": [<AUTH_CONNECTIONS>],
      "adminEmail": "<ADMIN_EMAIL>",
      "allowedLoginEmailDomains": [<DOMAINS>]
    }
  }'
```

**Auth connections format:**
- Google: `[{"googleAuthConnection": {}}]`
- Password: `[{"passwordAuthConnection": {}}]`
- Both: `[{"googleAuthConnection": {}}, {"passwordAuthConnection": {}}]`

**Success response (HTTP 200):**
```json
{ "name": "accounts/...", "externalId": "...", "loginId": "my-workspace", "displayName": "My Workspace" }
```

Tell the user:
> Your workspace **<displayName>** has been created!
> Workspace ID: `<loginId>`
> Region: <region>
>
> You can access it at: https://confidence.spotify.com

**Error handling:**

| HTTP Status | Meaning | User message |
|---|---|---|
| 400 + "work email" | Free email rejected | "Confidence requires a work email address. Free providers like Gmail aren't allowed." |
| 400 + "already have an account" | Logged-in Auth0 user already has account | "This login already has a Confidence account. Log in with a different email to create a new workspace." → re-run Step 1 |
| 400 + code 9 | Account under review | see "Under review handling" below — **do NOT assume email verification** |
| 400 | Other validation error | Parse `.message`, show in plain English, re-collect the invalid field |
| 401 | Token expired/invalid | "Session expired. Let me log you in again." → re-run Step 1 |
| 409 | Name already taken | "That workspace name was just taken. Let's pick another." → re-run Step 2 |
| 504 / timeout | gRPC deadline exceeded | Retry up to 3 times with 3-second delays. If it still fails, tell the user: "The server is taking longer than usual. Let me try once more." |
| 500+ | Server error | "Something went wrong on our end. Let me try again in a moment." |

**Under review handling (code 9):**

Code 9 means the account is "under review" — but the **reason** varies. Parse the `.message` field to determine the cause:

1. **Email not verified** (message contains "verify" or "email"): Tell the user: "Please check your email for a verification link from Confidence and confirm your address. Let me know once you've done that!"

2. **Account flagged/blocked** (message contains "fraud", "flagged", "blocked", "suspicious", or doesn't match #1): Tell the user: "Your account has been flagged for review. This usually resolves quickly. If it persists, contact support at confidence-support@spotify.com."

3. **Generic "under review"** with no clear cause: Tell the user: "Your account is under review. This can happen for a few reasons — please check your email for any messages from Confidence. If you need help, contact confidence-support@spotify.com."

**For case #1 only (email verification)**, after the user confirms, retry 4 times with 2-second delays in a single Bash command:

```bash
for i in 1 2 3 4; do
  RESP=$(curl -s -w "\n%{http_code}" --max-time 120 -X POST "https://onboarding.confidence.dev/v1/accounts" \
    -H "Authorization: Bearer $(cat $TMPDIR/confidence_token)" \
    -H "Content-Type: application/json" \
    -d '<SAME_BODY>')
  HTTP=$(echo "$RESP" | tail -1)
  BODY=$(echo "$RESP" | sed '$d')
  echo "ATTEMPT $i: HTTP=$HTTP"
  echo "$BODY"
  if [ "$HTTP" = "200" ]; then echo "SUCCESS"; break; fi
  if [ "$HTTP" != "400" ] || ! echo "$BODY" | grep -q "under review"; then echo "DIFFERENT_ERROR"; break; fi
  if [ "$i" -lt 4 ]; then sleep 2; fi
done
```

For cases #2 and #3, do NOT auto-retry — the issue won't resolve by retrying. Wait for the user to indicate they want to try again.

If all 4 retry attempts still return "under review", tell the user: "Verification hasn't propagated yet. Please wait a moment and let me know when you'd like to try again."

### Step 5: Get account-scoped token

The token from Step 1 has no `org_id` (it was issued before the account existed). The signup client's refresh token **cannot** be exchanged for an org-scoped token — Auth0 rejects cross-client refresh, and the signup client doesn't support org-scoping. A browser auth with the regular client is required.

**Use the browser auth script** with the **regular client ID** and the new org. The browser session from Step 1 is still active, so Auth0 auto-completes — the user sees no extra login prompt:
```bash
lsof -ti:8084 | xargs kill -9 2>/dev/null; python3 <SKILL_BASE_DIR>/auth.py 2fG3H4RhlAbIZm9Rfn32zTaILH7w1X4w <loginId_from_Step_4>
```

The response token will contain `org_id`, `account_name`, and `region` claims. Parse the TOKEN and REFRESH_TOKEN from stdout, then **save them in a separate `dangerouslyDisableSandbox: true` Bash call**:

```bash
echo "<ORG_SCOPED_TOKEN>" > "$TMPDIR/confidence_token" && echo "<REFRESH_TOKEN>" > "$TMPDIR/confidence_refresh_token"
```

**This save call MUST use `dangerouslyDisableSandbox: true`** — even though it doesn't need network access — so that `$TMPDIR` resolves to the same path that future curl calls will use. A sandboxed save writes to a different TMPDIR and the token will be invisible to non-sandboxed curl calls.

Tell the user:
> Connecting to your new workspace... (your browser will briefly open and close automatically — no action needed)

### Step 6: Done

Show a summary and next steps:

```
═══════════════════════════════════════════════════════════════
  Welcome to Confidence!
═══════════════════════════════════════════════════════════════

  Workspace: <displayName> (<loginId>)
  Region:    <region>
  Admin:     <adminEmail>
  URL:       https://confidence.spotify.com

  Next steps:
  • Run the setup wizard:    /onboard-confidence setup-wizard
  • Invite team members:     /onboard-confidence invite-user
  • Set up data warehouse:   /onboard-confidence setup-warehouse
  • Create a feature flag:   Ask me or use the Confidence UI
  • Integrate your app:      Ask me for SDK setup instructions
  • Learn experimentation:   /onboard-confidence learn

═══════════════════════════════════════════════════════════════
```

---

## Sub-command: invite-user

**Requires MCP.** If the confidence-flags MCP is not authenticated, prompt the user to run `/mcp` and click Authenticate next to confidence-flags first.

### Step Tracker

```
───── Invite User ─────────────────────────────────────────
  [1] Verify MCP          ○ pending
  [2] Target account     ○ pending
  [3] Invitation details ○ pending
  [4] Send invitation    ○ pending
────────────────────────────────────────────────────────────
```

### Step 1: Verify MCP

Call `mcp__confidence-flags__getIdentityInfo` (no args). If it succeeds, MCP is connected — proceed. If it fails, tell the user:
> To invite users, I need the Confidence tools connected. Type **`/mcp`** and click **Authenticate** next to **confidence-flags**.

Wait for the user to confirm, then retry.

### Step 2: Target account

Call `mcp__confidence-flags__getIdentityInfo` (no args) — returns current user's identity and account. If the user has multiple account memberships, ask which one.

Tell the user which account will receive the invitation.

### Step 3: Invitation details

Ask for:

1. **Email address(es)** — required. Accept a single email or a comma-separated list for batch invites.
   Validate email format locally.

2. **Send invitation email?** — default yes.
   > Should Confidence send an invitation email? (yes/no, default: yes)

### Step 4: Send invitation

For each email address, call:
```
mcp__confidence-flags__inviteUser({
  email: "<EMAIL>",
  disableInvitationEmail: "<true|false>"
})
```

For single invite, tell the user:
> Invitation sent to **user@example.com**!
> They'll receive an email with instructions to join.

For batch invites, show a summary table:
```
Invitations sent:
  ✓ alice@example.com — sent
  ✓ bob@example.com   — sent
  ✗ charlie@invalid   — invalid email address
```

**Error handling:** Parse the MCP tool error message and show in plain English. Common errors: invalid email, no admin permission, user already invited.

---

## Sub-command: create-client

**Requires MCP.** If the confidence-flags MCP is not authenticated, prompt the user to run `/mcp` and click Authenticate next to confidence-flags first.

### Step Tracker

```
───── Create Client ───────────────────────────────────────
  [1] Verify MCP         ○ pending
  [2] Client details     ○ pending
  [3] Create & credentials ○ pending
────────────────────────────────────────────────────────────
```

### Step 1: Verify MCP

Call `mcp__confidence-flags__getIdentityInfo` (no args). If it fails, prompt MCP auth (same as invite-user Step 1).

### Step 2: Client details

Ask the user what to name the client and pick a type:

> What should we call this client? (e.g., "iOS App", "Web Frontend", "Backend Service")

Then use AskUserQuestion for client type:
- **Frontend** — browser/mobile apps
- **Backend** — server-side services

### Step 3: Create & credentials

Call `mcp__confidence-flags__createClient` with `displayName` and `clientType`.
Then call `mcp__confidence-flags__getClientSecret` with the `clientName` from the response.

The secret is only returned once — show it to the user:

```
═══════════════════════════════════════════════════════════════
  Client Created
═══════════════════════════════════════════════════════════════

  Name:    <CLIENT_NAME>
  Secret:  <CLIENT_SECRET>

  Use this secret in your SDK configuration to resolve flags.
  Keep it safe — you can regenerate it, but the old one will
  stop working.

  Next: Ask me for SDK integration instructions, or run
        /onboard-confidence setup-wizard

═══════════════════════════════════════════════════════════════
```

---

## Sub-command: setup-wizard

Guided walkthrough of the full onboarding checklist. After account creation or sign-in, requires MCP authentication — all flag, client, and resource operations use MCP tools.

### User input style

**Always use AskUserQuestion** with selectable options for choices (up/down/enter). Only ask the user to type free-text when collecting names, emails, or other open-ended input. Never present numbered lists in plain text when AskUserQuestion can be used instead.

### Step Tracker

```
───── Setup Wizard ────────────────────────────────────────
  [1] Get started        ○ pending
  [2] Connect tools      ○ pending
  [3] Create client      ○ pending
  [4] Create flag        ○ pending
  [5] Add targeting      ○ pending
  [6] Test resolve       ○ pending
  [7] Done               ○ pending
────────────────────────────────────────────────────────────
```

### Step 1: Get started

If the user already answered "create account" vs "sign in" (e.g., from the default onboarding flow), use that answer — do NOT re-ask.

Otherwise (when entered directly via `/onboard-confidence setup-wizard`), use AskUserQuestion:
- **Create a new account** — I'll walk you through signup
- **Sign in to an existing account** — I already have one

**If "Create a new account":**
Run the full `create-account` sub-command flow (Steps 1–6 from that section). This handles signup, workspace creation, and re-auth with an org-scoped token. Once complete, proceed to Step 2 of setup-wizard with the token and region already set.

**If "Sign in to existing account":**
Check if a token file exists at `$TMPDIR/confidence_token` and is valid. If not, run the bundled auth script with the **regular client ID** (`2fG3H4RhlAbIZm9Rfn32zTaILH7w1X4w`). Validate the token, extract the region, and proceed to Step 2.

Determine the region from the token — this sets the API base URLs:
- EU: `flags.eu.confidence.dev`, `resolver.eu.confidence.dev`, `iam.eu.confidence.dev`
- US: `flags.us.confidence.dev`, `resolver.us.confidence.dev`, `iam.us.confidence.dev`

### Step 2: Connect tools

**This step is required.** All subsequent operations use MCP tools, so the user must authenticate the confidence-flags MCP before proceeding.

Tell the user:
> Before we create your first flag, let's connect the Confidence tools. This gives you full flag management right inside Claude Code.
>
> Type **`/mcp`** in the prompt, then click **Authenticate** next to **confidence-flags**. Your browser session from login will auto-complete — no extra password needed.

**Do NOT wait for explicit confirmation.** The `/mcp` command output arrives as a `<local-command-caveat>` message that the agent cannot act on directly, but the user's next message (even just "done", "continue", or any text) means they have completed the step. As soon as ANY user message arrives after showing the `/mcp` instruction, immediately call `mcp__confidence-flags__getIdentityInfo` (no args) to verify the connection and proceed to Step 3 without asking further questions.

**If MCP call fails**, tell the user the tools aren't connected yet and ask them to try `/mcp` again. Do NOT proceed without MCP.

### Step 3: Create client

Check if the user already has a client by calling `mcp__confidence-flags__listClients`.

If clients exist, use AskUserQuestion to let the user pick one or create a new one. If none exist, ask for a client name and type:

> What should we call this client? (e.g., "iOS App", "Web Frontend", "Backend Service")

Then use AskUserQuestion for client type:
- **Frontend** — browser/mobile apps
- **Backend** — server-side services

Call `mcp__confidence-flags__createClient` with `displayName` and `clientType`.
Then call `mcp__confidence-flags__getClientSecret` with the `clientName` to get the secret.

Save the client `name` and `clientSecret` for later steps.

### Step 4: Create flag

EDUCATE then ASK:
> A feature flag controls a piece of functionality. Let's create your first one.
> What should it be called? (e.g., "new-checkout-flow", "dark-mode")

Validate: 4-63 chars, `[a-z0-9-]`.

Use AskUserQuestion for variant type:
- **Simple on/off (boolean)** — two variants: on and off
- **Custom variants** — I'll name my own

The MCP `createFlag` tool handles schema, variants, AND client attachment in one call:

For on/off:
```
mcp__confidence-flags__createFlag({
  flagName: "<FLAG_NAME>",
  clientName: "<CLIENT_NAME>",
  schemaObject: '{"enabled": "boolean"}',
  variants: '[{"name": "on", "value": {"enabled": true}}, {"name": "off", "value": {"enabled": false}}]'
})
```

For custom variants, infer the schema from what the user describes and pass it similarly.

### Step 5: Add targeting

EDUCATE:
> Targeting rules control who sees which variant. Let's set a default — you can add more rules later.

Use AskUserQuestion to pick the default variant (list the variants created in Step 4).

**Before adding the rule**, call `mcp__confidence-flags__getContextSchema` with the client name to discover the available entity fields. Use the first available entity field (e.g., `visitor_id`) as the `targetingKey`. Do NOT assume `user_id` or `targeting_key` — new accounts typically only have `visitor_id`.

The MCP `addTargetingRule` tool handles segment creation internally:
```
mcp__confidence-flags__addTargetingRule({
  flagName: "<FLAG_NAME>",
  variantAllocations: '{"<DEFAULT_VARIANT>": 100}',
  targetingKey: '<ENTITY_FIELD_FROM_CONTEXT_SCHEMA>'
})
```

### Step 6: Test resolve

EDUCATE:
> Let's verify the flag works by resolving it for different contexts.

**Test all targeting cases.** If the flag has targeting rules that depend on context fields (e.g., `country`), resolve with context values that exercise EACH rule — both matching and non-matching cases. For example, if the rule is "on when country is not US", test with `country: "SE"` (should match → on) AND `country: "US"` (should not match → off/default). Show results for all cases in a summary table.

Make parallel resolve calls for each test case:
```
mcp__confidence-flags__resolveFlag({
  flagName: "<FLAG_NAME>",
  clientName: "<CLIENT_NAME>",
  entity: "targeting_key",
  entityValue: "test-user-1",
  context: '{"<CONTEXT_FIELD>": "<MATCHING_VALUE>"}'
})

mcp__confidence-flags__resolveFlag({
  flagName: "<FLAG_NAME>",
  clientName: "<CLIENT_NAME>",
  entity: "targeting_key",
  entityValue: "test-user-1",
  context: '{"<CONTEXT_FIELD>": "<NON_MATCHING_VALUE>"}'
})
```

Show results in a summary:
```
  Test Results:
    country = SE  → variant "on"  (enabled: true)   ✓
    country = US  → variant "off" (enabled: false)   ✓
```

If resolve fails or returns no match, check that:
1. The flag is attached to the client
2. Rules are enabled
3. Context fields required by targeting rules are included in the resolve call
4. A catch-all rule exists for non-matching contexts (otherwise they fall through to code default)

### Step 7: Done

Show a summary, then offer SDK integration using the **confidence-docs MCP**:

```
═══════════════════════════════════════════════════════════════
  Setup Complete!
═══════════════════════════════════════════════════════════════

  Client:   <CLIENT_NAME>
  Secret:   <CLIENT_SECRET>
  Flag:     <FLAG_NAME>
  Variants: <VARIANT_LIST>
  Default:  <DEFAULT_VARIANT>

  Your flag is live and resolving!

═══════════════════════════════════════════════════════════════
```

Use AskUserQuestion for next steps:
- **Integrate the SDK** — get code snippets for your platform
- **Invite team members** — add collaborators to your workspace
- **Set up data warehouse** — connect analytics pipeline
- **Create more flags** — keep building
- **Learn experimentation** — interactive course on A/B testing

**If the user picks "Integrate the SDK"**, use `mcp__confidence-docs__getCodeSnippetAndSdkIntegrationTips` with the user's platform (ask via AskUserQuestion: JavaScript, Python, Java, Kotlin, Swift, Go, React) to provide tailored integration code. This gives the user the exact SDK setup they need.

**For other choices**, direct to the corresponding sub-command.

---

## Sub-command: setup-warehouse

This command has been split into dedicated skills for each warehouse type. When the user asks to set up a warehouse, use `/onboard-confidence:setup-warehouse` which will guide them to the right one:
- `/onboard-confidence:setup-warehouse-bigquery`
- `/onboard-confidence:setup-warehouse-snowflake`
- `/onboard-confidence:setup-warehouse-databricks`
- `/onboard-confidence:setup-warehouse-redshift`

---

## Sub-command: learn

Interactive learning about experimentation concepts. The skill teaches, asks questions, and the user answers — like a guided course.

### Topics

| Topic | Category | What it covers |
|-------|----------|----------------|
| Statistics | STATS | Statistical significance, p-values, confidence intervals, sample size |
| Experiment Design | DESIGN | Hypothesis formation, control/treatment, randomization, bias |
| Feature Flags | FLAGS | Flag types, targeting rules, rollouts, kill switches |
| Metrics | METRICS | Metric types, guardrails, primary/secondary metrics, SRM |
| Coordination | COORDINATION | Mutual exclusion, layered experiments, interaction effects |

### Flow

1. **Pick a topic:**
   > What would you like to learn about?
   > 1. Statistics fundamentals
   > 2. Experiment design
   > 3. Feature flags
   > 4. Metrics
   > 5. Coordination

2. **Fetch content** — use `mcp__confidence-docs__searchDocumentation` to get relevant Confidence documentation for the chosen topic.

3. **Teach** — present a concept from the docs in 2-3 clear paragraphs. Use examples relevant to the user's product.

4. **Ask a question** — pose a comprehension question with multiple-choice answers:
   > **Question:** When running an A/B test, why is it important to determine sample size before starting?
   > 1. To make the test run faster
   > 2. To ensure you have enough statistical power to detect the expected effect
   > 3. To reduce server costs
   > 4. It's not important — you can stop whenever

5. **Evaluate the answer** — if correct, explain why. If wrong, explain the right answer and the reasoning.

6. **Track progress** — call the Learning API to record the user's answer:
   ```bash
   curl -s -X POST "https://onboarding.confidence.dev/v1/learningProgress:answerQuestions" \
     -H "Authorization: Bearer $(cat $TMPDIR/confidence_token)" \
     -H "Content-Type: application/json" \
     -d '{
       "course": "courses/<CATEGORY>",
       "questionUpdates": [{
         "lessonIndex": <LESSON>,
         "questionIndex": <QUESTION>,
         "currentAnswerIndex": <USER_ANSWER>
       }]
     }'
   ```

7. **Continue or finish** — after each question, ask if they want to continue or switch topics.

8. **Show progress** — at any time, fetch and display progress:
   ```bash
   curl -s "https://onboarding.confidence.dev/v1/learningProgress" \
     -H "Authorization: Bearer $(cat $TMPDIR/confidence_token)"
   ```

   ```
   ───── Learning Progress ────────────────────────────────────
     Statistics:     ██████░░░░ 3/5 lessons
     Design:         ████████░░ 4/5 lessons
     Feature Flags:  ██████████ 5/5 complete!
     Metrics:        ░░░░░░░░░░ not started
     Coordination:   ░░░░░░░░░░ not started
   ────────────────────────────────────────────────────────────
   ```

### Key principles

- **Use AskUserQuestion** for topic selection, quiz answers, and continue/switch decisions — selectable options, not typed numbers
- **Be conversational** — this is a dialogue, not a textbook
- **Use real examples** — tie concepts to the user's product/domain when possible
- **Encourage exploration** — if the user asks follow-up questions, answer them before moving on
- **Track everything** — every answer gets recorded via the Learning API so progress persists across sessions

---

## Sub-command: status

This is a lightweight command. Try MCP first (no REST auth needed if MCP is connected).

**If MCP is connected:**

1. Call `mcp__confidence-flags__getIdentityInfo` (no args)
2. Call `mcp__confidence-flags__listClients`
3. Display:

```
═══════════════════════════════════════════════════════════════
  Confidence Account Status
═══════════════════════════════════════════════════════════════

  Identity:  <displayName> (<email>)
  Account:   <accountName>
  Clients:   <list of clients>

  MCP Status:
    confidence-flags: ● connected
    confidence-docs:  ●

…(truncated)
