Skip to main content
Use autoplay_sdk.user_adoption_state to track a userโ€™s longer-term product maturity, independent of the per-session SessionState FSM.

What it is

SessionState tracks one conversationโ€™s moment-to-moment mode (thinking / proactive_assistance / reactive_assistance). user_adoption_state is a separate, longer-lived dimension keyed by user_id: where the user is in their lifecycle, how proficient they are, and how their onboarding is progressing. The journey and mastery values are an agent (LLM) decision via a versioned judge prompt; the resolved state is fed back into chat and proactive prompts so the assistant can adapt โ€” more teaching during onboarding, lighter nudges for power users.
The SDK ships pure logic and prompt definitions: the state model, the explore-first gate, onboarding plans, the tour matcher, and versioned prompts. The acall_llm runners that call these prompts remain integration-owned. For persistence, the SDK now also ships optional storage adapters (Redis / in-memory, plus write-only Amplitude / PostHog sinks) and session lifecycle helpers โ€” see State storage & session capture.

The two axes

Adoption is modeled as two independent axes set by the same judge call. A user can be onboarded yet still intermediate, or onboarding yet a fast proficient.
AxisEnumValues
Lifecycle stage (coarse)JourneyStateonboarding, onboarded
Competence (fine)MasteryLevelnovice, beginner, intermediate, proficient, power_user
Per-flow onboarding progress uses a third enum, because completion is inferred asynchronously from the live activity stream (rarely a clean 1:1 event):
EnumValues
WorkflowStatusnot_started, in_progress, completed

State model

UserAdoptionState

FieldTypePurpose
user_idstrMandatory scope key. Raises if empty.
metadatadict[str, Any]Fully dynamic, schema-less identity bag. No fixed keys; values must be JSON-serializable.
journey_stateJourneyStateCoarse lifecycle stage. Defaults to onboarding.
masteryMasteryRating (0-10) + level + reason.
discovered_featureslist[str]Features the user has already discovered.
onboardingOnboardingStateWelcome lifecycle + per-flow progress.
first_seen_at / last_updated_at / last_judged_atfloatLifecycle timestamps.
role is not special-cased in storage โ€” it is just one conventional metadata key. A thin state.role convenience property reads metadata.get("role").

Sub-objects

  • Mastery โ€” rating: int (0-10, clamped), level: MasteryLevel, description: str.
  • WorkflowProgress โ€” the agentโ€™s evolving belief about one target flow: status, confidence (0-1), evidence, and first_detected_at / completed_at / last_evaluated_at. Also holds a steps: dict[str, StepProgress] map of per-step beliefs, with completed_step_ids and an upsert_step_progress(step_id, *, status, confidence, evidence="", now=None) mutation (write-once completed_at, mirroring upsert_workflow_progress).
  • StepProgress โ€” the agentโ€™s belief about one step of a workflow: step_id, status (same WorkflowStatus enum), confidence (0-1), evidence, completed_at, last_evaluated_at. A deliberate parallel to WorkflowProgress, one level down. Steps are signal for the LLM and for explainability โ€” they never gate workflow completion (see Onboarding plans).
  • StepInAssessment โ€” the latest โ€œshould I step in?โ€ decision: should_step_in, timing_assessment, reason, suggested_workflow_ids. Persisted whether or not anything was sent, so timing is explainable.
  • OnboardingState โ€” welcomed, distinct_features_seen, explore_gate_passed, a workflow_progress: dict[str, WorkflowProgress] map, last_step_in, cooldown/cadence timestamps, and proactive_support_count. Derived helpers: completed_workflow_ids, in_progress_workflow_ids, outstanding_workflow_ids(plan_ids). Mutations: upsert_workflow_progress(...), record_step_in(...).

Persistence and prompt injection

to_dict() / from_dict() round-trip the whole record (with _v snapshot versioning and defensive enum coercion, so an unknown enum value degrades to a safe default rather than raising). to_prompt_block() renders a compact [USER ADOPTION STATE] block (role + journey + mastery + a one-line guidance hint) to prepend to a chat prompt.

Explore-first gate

evaluate_hard_gate() is a pure, side-effect-free hard floor that guarantees a user is never interrupted too early. Intelligent timing on top of the floor is the LLMโ€™s job.
It returns (passed, reason) and blocks when the role is not allowlisted, the user has not explored enough yet, they are already onboarded, a cooldown is in effect, or the nudge cap is reached. When it returns True, control passes to the LLM to decide whether now is the right moment and what to say.

Onboarding plans

The host configures, per product (and optionally per role), the set of workflows a user should complete. The agent is handed this list and reasons about progress against it โ€” it is never hardcoded in the SDK. Each workflow can optionally declare an ordered list of steps โ€” the things a user typically does to achieve the workflow. Steps are context and signal for the agent (they describe what โ€œdoneโ€ looks like so the LLM can recognise progress in the live activity stream); they are not a hard checklist. A user may finish a workflow via a variant path without hitting every step.
Each workflow_id lines up with a tour id so a suggested workflow can be delivered as a โ€œshow meโ€ tour offer.

Steps

A WorkflowStep has step_id (required), name, description, and required (default True). required is only a hint to the LLM about which steps are core vs. nice-to-have โ€” it does not gate completion. OnboardingWorkflow.required_step_ids lists the required step ids in declared order. A workflow with no declared steps behaves exactly as before (the LLM judges completion from activity alone), so steps are fully backward compatible. Steps are configured in the product config under integration_config.onboarding_workflows[].steps and round-trip through to_dict() / from_dict() (the steps key is omitted when empty). Per-user, the agent records its belief about each step in WorkflowProgress.steps[step_id] (a StepProgress) via wp.upsert_step_progress(...).

Completion

split_workflows() is a deterministic prefilter/guardrail โ€” the authoritative completion call is the LLMโ€™s, reflected in WorkflowProgress.status. Steps never gate this: a workflow counts as complete iff the LLM set its status to completed, regardless of per-step state.
  • is_plan_complete(plan, workflow_progress) โ€” a thin convenience wrapper over split_workflows returning True when there are no outstanding workflows (an empty plan is never โ€œcompleteโ€).
  • step_completion_ratio(workflow, wp) โ€” the fraction of a workflowโ€™s declared steps the LLM currently believes are completed. For display/telemetry only (e.g. a progress bar); never used to decide completion. Returns 0.0 for a workflow with no declared steps or when wp is None.

Query to tour matching

A reusable primitive that generalizes role-filtered tour selection. The SDK builds the structured prompt input and parses/validates the LLM output; the acall_llm call itself is integration-owned.
parse_tour_match_output() validates returned ids against the role-filtered catalog and falls back to an empty result on malformed JSON, so a bad model response never raises into the caller.

Prompts

Three versioned prompts ship in autoplay_sdk.prompts (each a dict with name / description / version / content, all returning a single JSON object). Call them via your integrationโ€™s LLM client with response_format={"type": "json_object"}.
ConstantNameDecides
ADOPTION_STATE_JUDGE_PROMPTadoption_state_judge (v0.1)journey_state + mastery from aggregated activity.
TOUR_MATCH_PROMPTtour_match (v0.1)The primary + related tours for a query.
ONBOARDING_WELCOME_PROMPTonboarding_welcome (v0.1)Per-workflow completion, whether/when to send a welcome, and the next-flow โ€œshow meโ€ offers.

See also