Skip to content

Onboarding

View as Markdown

TIE’s onboarding agent collects a user’s identity fields through a natural conversation before their memory graph is established. All apps on the platform share the same unified field set — a user who onboarded in Vibrantly won’t be re-asked for their name in BlinkLife.

AI Coding Prompt

Drop this into Claude, Codex, or any AI coding assistant to implement TIE onboarding for your app.

Implement TIE onboarding following these rules:

TIE's onboarding agent collects a user's identity fields through a natural one-question-
at-a-time conversation before their memory graph is established. All apps on the platform
share the same unified field set so users are never re-asked for information they already
gave in another app.

BASE URL: https://your-tie-host

AUTH:
- All endpoints take the user's TIE Bearer token (the same token you use for chat
  completions). Issue these calls from your backend with the user's token in scope —
  never with a long-lived service credential.

---

STEP 1 — START THE ONBOARDING CONVERSATION

  POST /v1/chat/completions
  Authorization: Bearer $USER_TOKEN
  X-Agent-Id: onboarding
  Content-Type: application/json

  {
    "model": "anthropic/claude-sonnet-4-6",
    "messages": [{ "role": "user", "content": "Hi" }],
    "stream": true,
    "persona_id": "<user's active persona UUID>",
    "skill_instructions": "<JSON string — see below>"
  }

  persona_id: the UUID of the user's active persona (from GET /personas, is_preset: false).
    Pass this so the onboarding agent speaks in the user's chosen persona voice. If omitted,
    the agent falls back to a generic default voice. Always pass it when the persona exists.

  skill_instructions fields (serialize as JSON string):
    questions: array of question objects — your question list.
      Each question object:
        id       (required) — field ID. Unified IDs (preferred_name, date_of_birth,
                   location) override the default text; any other ID is app-specific.
        question (required) — question text shown to the user.
        hint     (optional) — inline guidance for the agent about what counts as a valid
                   answer. E.g. "must be a real city, country, or region on Earth".
        optional (optional, boolean) — when true, the agent signals the user can skip
                   this field, and onboarding_complete succeeds without it.
        multi    (optional, boolean) — when true, the agent asks "anything else?" once
                   to collect multiple values.
    already_captured: object of { field_id: value } — fields you already know.
      TIE skips re-asking these. Always pre-fill every field you already have —
      from a signup form, OAuth profile, age-verification screen, or a prior app session.
      If you collected date_of_birth during signup and omit it here, the agent will ask
      for it again mid-conversation, which feels broken to the user.
    acknowledge_source_app: boolean — when true, TIE opens with
      "I already know a bit about you" for users who onboarded in another app.
    include_unified_fields: boolean, default true — set false to ask ONLY your questions.
      Unified fields you don't list are then neither asked nor required by
      onboarding_complete. List a unified ID in questions to still ask it.
    rules: string — your own instructions for the agent (e.g. "ask each question exactly
      as written, never follow up"). Added after TIE's base onboarding rules and win where
      they conflict. They can't change the tool calls described in step 2.
    confirm_before_complete: boolean, default true — set false to have the agent send
      onboarding_complete as soon as the last question is answered, instead of first
      asking "anything you'd like to adjust, or shall we wrap?".

  skill_instructions must be the JSON string itself. Text that doesn't start with "{"
  is ignored. To keep your onboarding wording in Truememory instead of your app, store
  it as a skill and name it in the request ("skills": ["your-skill-key"]): its prompt
  is added ahead of "rules", so editing the skill changes the agent without an app
  release. Questions and the flags above still come from skill_instructions.

  Example skill_instructions (as JSON string):
    {
      "questions": [
        { "id": "date_of_birth", "question": "When were you born?",
          "hint": "a date — e.g. 1993, March 1993, or a full date like Feb 10 1993" },
        { "id": "location", "question": "Where in the world are you based?",
          "hint": "a recognisable city, country, or region on Earth" },
        { "id": "currentFocus", "question": "What are you focused on right now?",
          "optional": true },
        { "id": "expectations", "question": "If I could help with one thing, what would it be?",
          "optional": true }
      ],
      "already_captured": { "preferred_name": "Julio", "date_of_birth": "1993-05-14" },
      "acknowledge_source_app": false
    }

---

STEP 2 — WATCH THE SSE STREAM

  The response is an OpenAI-compatible SSE stream. Each data: line carries a
  chat.completion.chunk JSON payload. Look for choices[0].delta.tool_calls[].

  Four events to handle:

  a) capture_field — fires after each answer (UX only):
     { "function": { "name": "capture_field", "arguments": "{\"field_id\":\"location\",\"value\":\"Kuala Lumpur\"}" } }
     Use this ONLY for live UI feedback (checkmarks, progress indicators).
     Do NOT persist data from capture_field — the user may correct answers.

  b) skip_field — fires when the user explicitly opts out of a question:
     { "function": { "name": "skip_field", "arguments": "{\"field_id\":\"currentFocus\",\"reason\":\"privacy\"}" } }
     The field is omitted from the final onboarding_complete payload. You can store
     the skip as an opt-out signal in your own DB if needed.

  c) onboarding_complete — terminal event, fires once when all required fields are collected
     (and, unless confirm_before_complete is false, the user has confirmed the agent's summary):
     { "function": { "name": "onboarding_complete", "arguments": "{\"fields\":{\"preferred_name\":\"Julio\",\"date_of_birth\":\"1995-05-23\",\"location\":\"Kuala Lumpur\"}}" } }
     This is the primary success event. Show a confirmation screen with the collected fields.
     Fields marked optional that the user skipped are absent from this payload.

  d) onboarding_aborted — terminal event, fires in three cases:
       1. The user chooses to stop ("stop", "later", "not now").
       2. Your skill_instructions JSON is malformed (TIE can't validate the field set).
       3. The conversation hits the 20-turn safety cap without reaching onboarding_complete.
     { "function": { "name": "onboarding_aborted", "arguments": "{\"fields\":{\"preferred_name\":\"Julio\"},\"reason\":\"user_stopped\"}" } }
     The reason field tells you which case fired:
       - "user_stopped" / free-form text → user opted out
       - "max_turns_exceeded (N >= 20)"   → safety cap (rare; suggests prompt or LLM issue)
     fields contains only what was collected so far AND only fields TIE knows about
     (your skill_instructions question IDs + the unified set) — unknown IDs are dropped.
     Treat this as PARTIAL data: do NOT show a confirmation screen or write anything
     to user-metadata automatically. Route the user to a fallback (manual form or
     retry prompt) rather than silently dropping them.

---

STEP 3 — USER CONFIRMS: WRITE METADATA

  After the user confirms on your confirmation screen, write to TIE Auth using the
  same user Bearer token you used for the onboarding conversation:

  a) Unified fields (shared across all TIE apps) — wrap them in a `metadata` object:
     PATCH /auth/me
     Authorization: Bearer $USER_TOKEN
     Content-Type: application/json

     { "metadata": { "preferred_name": "Julio", "date_of_birth": "1995-05-23", "location": "Kuala Lumpur" } }

     Unified field IDs: preferred_name, date_of_birth, location
     Stored in raw_user_meta_data — visible to all apps.

  b) App-specific fields — body is the patch itself:
     PATCH /auth/me/app-metadata?app=your_app
     Authorization: Bearer $USER_TOKEN
     Content-Type: application/json

     { "currentFocus": "Personal growth" }

     Stored in raw_app_meta_data[your_app] — isolated per app namespace.

  Both PATCH endpoints:
    - Are atomic shallow merges — keys you send are upserted, omitted keys are unchanged.
    - Accept null values to delete a top-level key (nested nulls are preserved as data).
    - Cap the request body at 16 KiB and 64 top-level keys (422 VALIDATION_ERROR if exceeded).
    - Return 404 if the user doesn't exist (no upsert).

---

STEP 4 — CREATE THE AGENT PERSONA

  See the Agent Creation guide. Summary:
  - GET /personas with the user's Bearer token, filter for is_preset: false.
  - If one exists: skip creation (agent already set up from another app).
  - If none: POST /personas → PUT /personas/active/{id} → PUT /v1/voice/preferences/{id}.

---

STEP 5 — MARK ONBOARDING COMPLETE

  Update your own app's database to record that this user has completed onboarding.
  Route the user into the app.

---

FIELD OWNERSHIP SUMMARY:

  Unified (raw_user_meta_data, shared):  preferred_name, date_of_birth, location
  App-specific (raw_app_meta_data[app]): any field ID not in the unified set

  TIE collects all unified fields by default — set include_unified_fields: false to
  skip the ones you don't list. App-specific questions always appear after unified fields.

---

IMPORTANT RULES:
- The onboarding agent has NO memory — it runs before the user's knowledge graph
  is established. Never use it for post-onboarding conversations.
- Only write data after onboarding_complete, not during capture_field events.
- Always handle onboarding_aborted as a terminal event — it can fire even without
  explicit user opt-out (malformed config, safety cap). Don't treat it as a stream error.
- Always show a confirmation screen before persisting anything.

Flow

1. Start onboarding conversation with TIE (pass already_captured + questions)
2. Stream turns until onboarding_complete fires
3. Show user confirmation screen
4. On confirm → write unified fields to user-metadata
                → write app-specific fields to app-metadata
                → create/update agent persona
5. Mark onboarding complete in your app

Field Ownership

Field ID Type Stored in
preferred_name Unified raw_user_meta_data
date_of_birth Unified raw_user_meta_data
location Unified raw_user_meta_data
currentFocus App-specific raw_app_meta_data[your_app]
expectations App-specific raw_app_meta_data[your_app]
(any custom ID) App-specific raw_app_meta_data[your_app]

Unified fields are collected once across all TIE apps — if a user answered them in another app, TIE skips re-asking. App-specific fields are isolated per app namespace.


Running the Onboarding Agent

Two auth patterns are supported — use whichever fits your architecture:

# Option A — client-side (mobile / browser): user's own token
POST https://your-tie-host/v1/chat/completions
X-Agent-Id: onboarding
Authorization: Bearer $USER_TOKEN
Content-Type: application/json

# Option B — server-side: service key on behalf of the user
POST https://your-tie-host/v1/chat/completions
X-Agent-Id: onboarding
Authorization: Bearer $SERVICE_KEY
X-On-Behalf-Of: $TIE_USER_ID
Content-Type: application/json

Request body (same for both):

{
  "model": "anthropic/claude-sonnet-4-6",
  "messages": [{ "role": "user", "content": "Hi" }],
  "stream": true,
  "skill_instructions": "{\"questions\": [{\"id\": \"preferred_name\", \"question\": \"What should I call you?\"}, {\"id\": \"date_of_birth\", \"question\": \"When were you born?\"}, {\"id\": \"location\", \"question\": \"Where in the world are you based?\"}, {\"id\": \"currentFocus\", \"question\": \"What are you currently focused on?\"}], \"already_captured\": {\"preferred_name\": \"Julio\"}, \"acknowledge_source_app\": false}"
}
skill_instructions field Description
questions Your question list. Each entry: id (required), question (required), hint (optional — guidance on what counts as valid), optional (optional bool — user can skip, completion succeeds without it), multi (optional bool — collects multiple values). Unified IDs (preferred_name, date_of_birth, location) override default text; any other ID is app-specific, appended after unified fields.
already_captured Fields already known — TIE skips re-asking these. Pre-fill any fields you collected before the conversation (e.g. DOB from a signup screen, name from OAuth). If you don’t, the agent will ask for data you already have.
acknowledge_source_app When true, TIE opens with “I already know a bit about you” for returning users
include_unified_fields Default true. Set false to ask only your questions — unified fields you don’t list are neither asked nor required by onboarding_complete.
rules Your own instructions for the agent, added after TIE’s base onboarding rules. They win where the two conflict (wording, acknowledgments, follow-ups) but can’t change the tool calls.
confirm_before_complete Default true: once every question is answered, the agent mirrors the answers back and asks whether to adjust or wrap. Set false to send onboarding_complete right after the last answer.

skill_instructions must be the JSON string itself. Text that doesn’t start with { is ignored.

To keep your onboarding wording in Truememory instead of your app, store it as a skill and name it in the request with "skills": ["your-skill-key"]. Its prompt is added ahead of rules, so editing the skill changes the agent without an app release. The questions and the flags above still come from skill_instructions, and skill_selection: "auto" doesn’t apply to onboarding.

Asking only your own questions

An app with its own short flow can turn off the unified fields and the confirmation turn. This asks exactly two questions and sends onboarding_complete after the second answer, while still using the user’s name from already_captured:

{
  "include_unified_fields": false,
  "confirm_before_complete": false,
  "rules": "Ask each question exactly as written, one per message. Never ask follow-ups.",
  "questions": [
    { "id": "currentFocus", "question": "What's the one thing you want to get better at?" },
    { "id": "obstacle", "question": "What usually gets in the way?" }
  ],
  "already_captured": { "preferred_name": "Julio" },
  "acknowledge_source_app": false
}

Watching the Tool Calls

The onboarding agent emits four observable tool calls. These appear in the same place regardless of whether you use streaming or non-streaming mode.

Streaming ("stream": true) — tool calls arrive as SSE delta chunks. Each data: line carries a chat.completion.chunk JSON payload. Look for choices[0].delta.tool_calls[]:

Non-streaming ("stream": false) — tool calls arrive in the final response body at choices[0].message.tool_calls[]. Same shape, no stream parsing needed. BlinkLife uses this mode.

// Field collected — update UI only (e.g. mark a question as ✓), persist nothing yet
{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_a","type":"function","function":{"name":"capture_field","arguments":"{\"field_id\":\"date_of_birth\",\"value\":\"1995-05-23\"}"}}]}}]}

// User skipped an optional field — store as opt-out signal if needed, do not require it
{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_b","type":"function","function":{"name":"skip_field","arguments":"{\"field_id\":\"currentFocus\",\"reason\":\"privacy\"}"}}]}}]}

// All required fields collected — show confirmation screen
{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_c","type":"function","function":{"name":"onboarding_complete","arguments":"{\"fields\":{\"preferred_name\":\"Julio\",\"date_of_birth\":\"1995-05-23\",\"location\":\"Kuala Lumpur\"}}"}}]}}]}

// User chose to stop — treat as partial, do not persist
{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_d","type":"function","function":{"name":"onboarding_aborted","arguments":"{\"fields\":{\"preferred_name\":\"Julio\"},\"reason\":\"user_stopped\"}"}}]}}]}

Only act on onboarding_complete. Do not write data or create the agent persona until the user confirms on your confirmation screen.

If you receive onboarding_aborted, treat the data as partial — do not write to user-metadata and do not show a confirmation screen. Route the user to a fallback such as a manual form or a retry prompt.


After Confirmation

# 1. Write unified identity fields (shared across apps) — wrap in `metadata`
PATCH https://your-tie-host/auth/me
Authorization: Bearer $USER_TOKEN
Content-Type: application/json

{
  "metadata": {
    "preferred_name": "Julio",
    "date_of_birth": "1995-05-23",
    "location": "Kuala Lumpur"
  }
}

# 2. Write app-specific fields — body is the patch itself
PATCH https://your-tie-host/auth/me/app-metadata?app=your_app
Authorization: Bearer $USER_TOKEN
Content-Type: application/json

{
  "currentFocus": "Personal growth"
}

# 3. Create or update the agent persona
# → See the Agent Creation guide

The onboarding agent has no memory — it runs before the user’s knowledge graph is established. Never write data based on capture_field events alone — the user may correct answers before onboarding_complete fires.


Manual Update (Skip the Agent)

If you already have the user’s data — from a signup form, a previous app, or a migration — you can write it directly without running the onboarding conversation at all. Use the same metadata endpoints:

# Unified identity fields (shared across all TIE apps) — wrap in `metadata`
PATCH https://your-tie-host/auth/me
Authorization: Bearer $USER_TOKEN
Content-Type: application/json

{
  "metadata": {
    "preferred_name": "Julio",
    "date_of_birth": "1995-05-23",
    "location": "Kuala Lumpur"
  }
}

# App-specific fields (isolated to your app namespace)
PATCH https://your-tie-host/auth/me/app-metadata?app=your_app
Authorization: Bearer $USER_TOKEN
Content-Type: application/json

{
  "currentFocus": "Personal growth"
}

Both endpoints do a shallow merge — keys you send are upserted, keys you omit are left unchanged. Pass null as a value to delete a key from the namespace.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close