> ## Documentation Index
> Fetch the complete documentation index at: https://docs.causeloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Onboarding Tutorial

> End-to-end guide for onboarding a new client — from provisioning the tenant to training the engine on real imported data and reviewing the first insights.

This tutorial takes you through the complete lifecycle of bringing a new client onto Causeloop: provisioning the tenant, the owner's first login, the training wizard (import your history, run the engine, review abstentions, see real results), inviting teammates, configuring alert rules, and generating the first report.

<Info>
  This tutorial covers both the **operator steps** (database provisioning, run by your platform team) and the **owner/user steps** (UI walkthrough, performed by the client). Steps that require database or API access are clearly marked.
</Info>

<Note>
  This page walks the same lifecycle as [Train and Infer Locally with Qwen](/guides/train-and-infer-local) — ingest → train → review → results — but driven from the UI wizard instead of `curl`. If you want to see every request/response on the wire for this exact lifecycle (including real timings and real abstentions), that guide is the API-driven version of this wizard.
</Note>

***

## Before you begin

Make sure you have:

* Access to the Causeloop PostgreSQL database (or ask your platform team to run the provisioning step)
* The owner's email address and display name
* A decision on which plan to assign (`free`, `starter`, `growth`, or `enterprise`)
* A spreadsheet (`.xlsx` or `.csv`) of historical issues to import — the wizard trains on real data, not synthetic seed data

***

## Step 1 — Provision the tenant

<Info>
  This step is performed by your platform team. If you are the workspace owner, skip to [Step 2](#step-2--owner-first-login).
</Info>

Causeloop provisions a new client in a single atomic database transaction using the `onboard_client()` function. This creates the organization, workspace, owner user, and membership in one call.

```sql theme={null}
SELECT * FROM onboard_client(
    p_org_name       => 'Acme Corp',
    p_org_slug       => 'acme',
    p_owner_email    => 'founder@acme.com',
    p_owner_name     => 'Dana Founder',
    p_workspace_name => 'Acme Engineering',
    p_plan           => 'growth',
    p_timezone       => 'America/New_York',
    p_seats          => 25
);
```

The function returns four identifiers:

```
        organization_id         |         workspace_id          |         owner_user_id          |         membership_id
--------------------------------+-------------------------------+--------------------------------+--------------------------------
 org_01KV1PX3NNV29S67D9NT816VBS | ws_01KV1PX3NQ3B497SHK7JTNT525 | usr_01KV1PX3NR96XJRGD48NNHH431 | mem_01KV1PX3NR9KJ4NDD096HA97J3
```

**Parameters:**

| Parameter          | Required | Notes                                                              |
| ------------------ | -------- | ------------------------------------------------------------------ |
| `p_org_name`       | Yes      | Display name of the organization                                   |
| `p_org_slug`       | Yes      | URL-safe unique identifier (e.g. `acme`) — must be globally unique |
| `p_owner_email`    | Yes      | Email address of the workspace owner                               |
| `p_owner_name`     | Yes      | Display name of the owner                                          |
| `p_workspace_name` | Yes      | Display name of the first workspace                                |
| `p_plan`           | No       | Defaults to `starter`                                              |
| `p_timezone`       | No       | IANA timezone string; defaults to `UTC`                            |
| `p_seats`          | No       | Seat cap; defaults to plan minimum                                 |

<Warning>
  A duplicate `p_org_slug` raises an error and rolls back the entire transaction. Verify the slug is unused before running the command.
</Warning>

**Verify the provisioning:**

```sql theme={null}
-- Confirm org and workspace
SELECT o.slug, o.plan, w.id AS workspace_id, w.timezone
FROM organizations o JOIN workspaces w ON w.organization_id = o.id
WHERE o.slug = 'acme';

-- Confirm owner membership
SELECT u.email, m.role, m.status
FROM memberships m JOIN users u ON u.id = m.user_id
WHERE m.workspace_id = (SELECT id FROM workspaces WHERE slug = 'acme-engineering');

-- Confirm onboarding is ready
SELECT current_step, is_complete
FROM onboarding_state
WHERE workspace_id = (SELECT id FROM workspaces WHERE slug = 'acme-engineering');
```

Expected: owner role `owner` / status `active`, `current_step = connect_source`, `is_complete = false`.

For the full provisioning runbook including bulk onboarding, offboarding, and GDPR erasure, see [Client Provisioning](/deploy-security/client-provisioning).

***

## Step 2 — Owner first login

<Steps>
  <Step title="Receive the invitation">
    The workspace owner receives a welcome email with a link to [app.causeloop.ai](https://app.causeloop.ai). The link opens the Causeloop sign-in page, backed by WorkOS AuthKit.
  </Step>

  <Step title="Sign in">
    The owner signs in with the email address set in `p_owner_email` (email/password or SSO). WorkOS issues an upstream access token; Causeloop never creates a user as a side effect of sign-in — the owner's user record and membership already exist from Step 1.
  </Step>

  <Step title="Land on the training wizard">
    The client exchanges the WorkOS token for a scoped Causeloop JWT via `POST /v1/auth/exchange`, then the owner lands on the training wizard at `/onboarding`. The wizard always starts at **Welcome** — step progress through the wizard's five steps is tracked client-side, not read back from a server-side "current step" field.
  </Step>
</Steps>

***

## Step 3 — Complete the training wizard

The wizard is five steps: **Welcome → Import → Train → Review → Results**. Every step drives a real API call against your own imported data — nothing in this flow is synthetic or precomputed. Unlike the legacy `onboarding_state` step machine referenced in Step 1's provisioning verification (which predates this wizard), progress through these five steps lives in the browser (`localStorage`); only two of the five steps also call the legacy step-completion endpoint, noted below.

<Steps>
  <Step title="Welcome">
    `GET /v1/onboarding` loads a workspace-state readout — `current_step`, `first_sync.issues_ingested`, and the count of `completed_steps` — shown for context underneath the wizard's own three-stage preview (Import / Train / Results). The call is best-effort: if it fails, the owner can still proceed. Clicking **Start training** advances to Import.
  </Step>

  <Step title="Import history">
    The owner uploads a `.xlsx`, `.xls`, or `.csv` export of historical issues. Parsing happens entirely client-side — nothing is sent to the server until the owner explicitly submits.

    Once parsed, the wizard auto-detects a column-to-field mapping (matching header aliases like "Issue ID" → `external_id`, "Unstructured Text" → `narrative`, "issue\_created" → `occurred_at`) and shows it as an editable dropdown per column. The mappable fields are: **External ID** and **Title** (both required to submit), **Narrative**, **Occurred at**, **Severity**, **Team**, **External URL**, or **Ignore this column**.

    A validation summary counts, before anything is sent:

    * **Missing narrative** — these rows will be blocked for engine analysis (a narrative-less issue is stored but never clusters or gets extracted)
    * **Missing `occurred_at`** — these rows degrade to `occurred_at_source: "logged_at"` (the ingest time is used as the point-process timestamp instead of the true occurrence time)

    Clicking **Submit** batches the mapped rows into groups of 50 and calls `POST /v1/ingest/batch` once per batch:

    ```json theme={null}
    { "records": [
      { "external_id": "ISS-1042", "title": "Payment webhook retried indefinitely",
        "narrative": "Webhook handler entered a retry loop after...",
        "occurred_at": "2026-05-02T14:11:00Z", "occurred_at_source": "occurred",
        "team": "payments", "source": "custom_api" }
    ] }
    ```

    Each batch responds with `{accepted, rejected, job_id, issue_ids, engine_status_summary}`. The wizard accumulates these across batches and renders, live:

    * **Accepted** / **Rejected** counts
    * **Blocked (no narrative)** — from `engine_status_summary.blocked_no_narrative`
    * Every `rejected[]` entry, by row number and field-level error (e.g. `occurred_at: required (or send occurred_at_source='logged_at')`) — a rejected record never aborts the rest of the batch, and a whole-batch network failure is surfaced honestly rather than silently dropped

    If at least one record was accepted, the wizard calls `POST /v1/onboarding/steps/connect_first_integration/complete` — the closest honest match in the legacy step machine — then **Continue to Train** becomes available.
  </Step>

  <Step title="Train">
    Clicking **Start engine run** calls `POST /v1/engine/runs` with `{"mode": "full", "trigger": "onboarding"}`, which returns `202` with `{run_id, status, scope_hash, mode, mode_effective, mode_degraded}`. The wizard then polls `GET /v1/engine/runs/{run_id}` every 1.5 seconds until the run reaches a terminal status (`succeeded`, `failed`, or `cancelled`).

    A live stage tracker shows all ten real pipeline stages, in order, each with a status icon, a duration once it succeeds, and up to two counters:

    | Stage                | What it does                                              |
    | -------------------- | --------------------------------------------------------- |
    | `feature_forge`      | Reads stored embeddings for imported issues               |
    | `graph_weave`        | Builds the similarity graph (`edge_count`)                |
    | `leiden_partition`   | Leiden community detection (`theme_count`, `loop_count`)  |
    | `dsu_crosscheck`     | Cross-checks the partition with a disjoint-set baseline   |
    | `loop_anchor`        | Anchors loops to stable IDs across runs                   |
    | `term_namer`         | Names each loop from its top terms                        |
    | `taxonomy_label`     | Matches loops to the taxonomy where possible              |
    | `criticality_rollup` | Projects loops into pattern rows and rolls up criticality |
    | `hazard_fit`         | Fits the recurrence hazard model per pattern              |
    | `alert_eval`         | Evaluates alert rules against the fresh predictions       |

    While the run is in progress, the step also surfaces a running count of extraction abstentions already queued for review (there is no dedicated per-workspace extraction-job endpoint, so the review queue's growing count is used as an honest proxy).

    On success, a model card shows `mode_effective` (with a "(degraded from incremental)" note if the requested mode couldn't be honored — expected on a workspace's first-ever run), the `manifest_hash`, the pinned `code_version`, and one hash per config kind in the run's manifest. On failure, the card shows the failed stage name, the error message, and a **Retry** button.

    **Continue to Review** is disabled until the run succeeds.
  </Step>

  <Step title="Review abstentions">
    Extraction, criticality, and root-cause abstentions land here honestly rather than the model guessing. The step loads `GET /v1/review-queue?status=open&limit=100` and `GET /v1/review-queue/stats`.

    Items are grouped for triage:

    * A pinned **Blocked — no narrative** group first, for every ingest-type item whose reason is `blocked_no_narrative` (the rows the Import step flagged)
    * Then one group per issue or pattern (`entity_type:entity_id`), each showing the resolved issue/pattern title

    Each abstained attribute renders as an expandable chip. Expanding it shows the evidence span (if any) and:

    * If the item carries a `prediction_set`, one button per candidate value (with its confidence score) — clicking one calls `POST /v1/review-queue/{id}/resolve` with `{chosen_value, note?}`
    * Otherwise (or for a blocked-narrative item), a free-text field — supplying the missing narrative text and submitting resolves the same way
    * A **Dismiss…** control on every chip, and a group-level **Dismiss all**, both requiring a reason and calling `POST /v1/review-queue/{id}/dismiss` with `{reason}`

    A **Calibration progress** strip shows `GET /v1/ai/calibration/{task}` for the three conformal-extraction tasks surfaced in onboarding — `extract:process_cadence`, `extract:customer_harm`, `extract:financial_loss_cents` — each with its resolved item count (`n_items`) and a "degraded" badge if the task hasn't reached target coverage.

    An empty queue shows "Queue is clear" — resolving everything is not required to proceed. **Continue to Results** is always available; unresolved items remain in the main Review Queue page afterward.
  </Step>

  <Step title="Results">
    The final step reads real inference output produced by the run just trained: `GET /v1/patterns` (top 6 by risk score), `GET /v1/predictions` (top 4 by probability), and `GET /v1/recommendations` (the top-ranked one).

    * **Top patterns** — cards show the taxonomy name (or fallback name), up to three real top-term chips, a risk badge (hidden when the score is null or the backend's `50` sentinel default — never shown as a fabricated number), and the member issue count.
    * **Predictions** — cards show the recurrence probability, formatted as `>99%` above 99% (rather than a falsely precise `100%`), a **CREST** badge when `crest_active` is true, and `T̂_R ≈ Nd` (median days to recurrence) when available.
    * **Top recommendation** — the highest-ranked recommendation with its business case. When a pattern's recurrence probability has saturated the hazard model's ceiling (both pre- and post-fix probabilities read `>99%`), the flat `ale_avoided_cents` figure floors at $0 regardless of how much the fix actually helps — so the card leads with the **events-avoided/yr** figure instead, with an explanatory note, rather than showing a misleading "$0 exposure avoided."

    Clicking **Finish** calls `POST /v1/onboarding/steps/review_first_pattern/complete`, then `POST /v1/onboarding/skip` to mark the workspace onboarding-complete (both best-effort — the wizard finishes locally even if either call fails) and routes to `/dashboard`.
  </Step>
</Steps>

<Tip>
  Every number in the wizard comes from your own imported data — there is no demo/synthetic mode. If you'd rather drive this exact lifecycle from the command line (useful for scripting a bulk onboarding, or for a local/self-hosted deployment with no UI), see [Train and Infer Locally with Qwen](/guides/train-and-infer-local) — it walks the same ingest → train → review → results lifecycle call-by-call, with real request/response bodies.
</Tip>

***

## Step 4 — Invite additional teammates

Teammates can be invited from **Settings → Members**, or directly via the API:

<Tabs>
  <Tab title="Via the UI">
    1. Go to **Settings → Members**
    2. Click **Invite member**
    3. Enter the email address and select a role
    4. Click **Send invite**

    The invitee receives an email link. When they accept, Causeloop creates their user record and membership automatically.
  </Tab>

  <Tab title="Via the API">
    ```bash theme={null}
    curl -X POST https://api.causeloop.ai/v1/invitations \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "email": "sre@acme.com",
        "role": "analyst"
      }'
    ```

    The role must be one of `admin`, `analyst`, or `viewer` (defaults to `viewer` if omitted). You can optionally
    pass `team_ids` to add the invitee to teams on acceptance. The endpoint
    returns the created invitation:

    ```json theme={null}
    {
      "id": "inv_01J...",
      "email": "sre@acme.com",
      "role": "analyst",
      "status": "pending"
    }
    ```
  </Tab>
</Tabs>

***

## Step 5 — Connect a live data source (optional)

The wizard's Import step is a one-time spreadsheet load — it does not connect an ongoing source. For continuous ingestion, connect a connector from **Settings → Integrations → Add connector**, independently of the wizard, at any time:

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/connectors \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "datadog",
    "display_name": "Production Datadog",
    "auth": {
      "api_key": "dd_api_...",
      "app_key": "dd_app_..."
    },
    "backfill_days": 30
  }'
```

Test the credentials before the first sync:

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/connectors/con_01J.../test \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{"success": true, "message": "Connected successfully", "latency_ms": 142}
```

Trigger the first sync manually if you don't want to wait for the schedule:

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/connectors/con_01J.../sync \
  -H "Authorization: Bearer $TOKEN"
```

***

## Step 6 — Review first insights

Once the imported batch (or a connector's first sync) completes, navigate to **Issues** and then **Patterns**.

**Issues view:** You will see the imported/synced failure events. Use the **Severity** and **Status** filters to focus on the most critical items. Click any issue to trigger an AI analysis (`POST /issues/{id}/analyze`) and see the root-cause explanation.

**Patterns view:** After Causeloop has processed enough issues, patterns begin to appear (immediately after a wizard-triggered engine run; asynchronously within minutes for connector-synced issues). Each pattern shows:

* The number of linked issues and the aggregate risk score
* The AI-generated root-cause summary
* A frequency chart with historical occurrences and a recurrence forecast
* The top recommendation

Click a pattern to open the detail view. If the pattern has a recurrence prediction, you will see the probability and forecast horizon on the right panel.

***

## Step 7 — Set up alert rules

Alert rules fire a notification when a prediction crosses a probability threshold:

<Tabs>
  <Tab title="Via the UI">
    Go to **Settings → Alert Rules → Create rule**. Set the probability threshold, the forecast window, and the notification channels (email, Slack, PagerDuty webhook).
  </Tab>

  <Tab title="Via the API">
    ```bash theme={null}
    curl -X POST https://api.causeloop.ai/v1/alert-rules \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "High recurrence risk",
        "scope": { "workspace": true },
        "threshold": {
          "probability": 0.75,
          "window_days": 7
        },
        "channels": ["email", "slack"]
      }'
    ```
  </Tab>
</Tabs>

Alert rules are evaluated continuously as new predictions are generated (including at the end of every engine run's `alert_eval` stage — see [Step 3](#step-3--complete-the-training-wizard)). When a rule fires, a notification is dispatched to all configured channels and appears in the **Activity Feed**.

***

## Step 8 — Generate the first report

Reports give stakeholders a narrative summary of issues, patterns, and recommendations over a time window.

Navigate to **Reports → New report** and select a time range (e.g. last 7 days). The report is generated asynchronously and includes:

* Issue volume by severity and source
* Active and newly detected patterns
* Top recommendations with expected loops prevented
* Risk forecast for the next period

You can export reports as PDF or share a link with `viewer`-role teammates.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="The owner cannot sign in after provisioning">
    Confirm that the email in `p_owner_email` exactly matches the email used during WorkOS sign-up (case-sensitive). If the user was pre-created with a different address, update `users.email` in the database so admission resolves against the right membership on next login.
  </Accordion>

  <Accordion title="Import step rejects every row">
    Check the **Column mapping** card — External ID and Title must both be mapped before submit is enabled. A common cause is the spreadsheet's header row not matching any auto-detected alias; remap the two required columns manually.
  </Accordion>

  <Accordion title="Every imported row lands in 'Blocked — no narrative' during Review">
    This means the mapped narrative column was empty (or unmapped) for those rows. Go back to Import, confirm a column is mapped to **Narrative (unstructured text)**, or resolve individual rows in the Review step by typing the missing narrative directly into the chip's free-text field.
  </Accordion>

  <Accordion title="Train step never leaves 'running'">
    Poll `GET /v1/engine/runs/{run_id}` directly — a stuck `feature_forge` stage usually means the imported issues have no embeddings yet (forge for narrative-bearing issues runs asynchronously right after ingest). Give it a minute after a large import before starting the run.
  </Accordion>

  <Accordion title="No issues appear after a connector sync">
    Check the sync run history: `GET /connectors/{id}/sync-runs`. A `status: error` entry will include a `details` field with the upstream error. Common causes: invalid credentials, insufficient permissions on the external tool, or a network firewall blocking outbound calls from the API to the integration.
  </Accordion>

  <Accordion title="Patterns don't appear after issues are ingested">
    Pattern detection only happens inside an engine run — for imported data, run (or re-run) Train; for connector-synced data, an auto-triggered run debounces a few seconds after ingestion. If patterns still don't appear, check that at least several issues share a common signal dimension (service name, error type, or team) — Causeloop requires a minimum cluster size to surface a pattern.
  </Accordion>

  <Accordion title="onboard_client() returns a duplicate slug error">
    Another organization already uses that slug. Choose a different `p_org_slug` and retry. The failed transaction leaves no partial records.
  </Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="Train and Infer Locally with Qwen" icon="terminal" href="/guides/train-and-infer-local">
    The API-driven version of this same wizard's lifecycle — every request and response on the wire, run against a real dataset.
  </Card>

  <Card title="Client Provisioning Runbook" icon="database" href="/deploy-security/client-provisioning">
    Full operator reference: bulk onboarding, offboarding, GDPR erasure, and verification queries.
  </Card>

  <Card title="Integrations Overview" icon="plug" href="/integrations/overview">
    Every connector type, credential formats, OAuth flows, and webhook setup.
  </Card>

  <Card title="Core Concepts" icon="book" href="/get-started/concepts">
    Roles, plans, seats, provenance, and abstention — the vocabulary this tutorial assumes.
  </Card>
</CardGroup>
