Skip to content

Personas

Permission required: personas

Full CRUD access to the user's personas (identity profiles), plus active persona switching and attached world book retrieval.

Usage

// List personas (paginated)
const { data, total } = await spindle.personas.list({ limit: 20, offset: 0 })

// Get a single persona
const persona = await spindle.personas.get('persona-id')
if (persona) {
  spindle.log.info(`Found: ${persona.name} (${persona.title})`)
}

// Get the default persona (is_default = true)
const defaultPersona = await spindle.personas.getDefault()

// Get the currently active persona
const active = await spindle.personas.getActive()
if (active) {
  spindle.log.info(`Active persona: ${active.name}`)
} else {
  spindle.log.info('No active persona')
}

// Create a persona (name is required)
const newPersona = await spindle.personas.create({
  name: 'Narrator',
  title: 'Omniscient storyteller',
  description: 'A neutral narrator who observes everything.',
  folder: 'Roleplay',
})

// Update a persona (all fields optional)
const updated = await spindle.personas.update(newPersona.id, {
  title: 'Omniscient but sarcastic storyteller',
  is_default: true,
})

// Switch the active persona
await spindle.personas.switchActive(newPersona.id)

// Deactivate (no active persona)
await spindle.personas.switchActive(null)

// Get the world book attached to a persona
const worldBook = await spindle.personas.getWorldBook(newPersona.id)
if (worldBook) {
  spindle.log.info(`Attached world book: ${worldBook.name}`)
}

// Delete a persona
const deleted = await spindle.personas.delete(newPersona.id)

Methods

Method Returns Description
list(options?) Promise<{ data: PersonaDTO[], total: number }> List personas. Options: { limit?, offset? }. Defaults: limit 50, max 200.
get(personaId) Promise<PersonaDTO \| null> Get a persona by ID. Returns null if not found.
getDefault() Promise<PersonaDTO \| null> Get the user's default persona (is_default = true). Returns null if none set.
getActive() Promise<PersonaDTO \| null> Get the user's currently active persona. Returns null if none is active.
create(input) Promise<PersonaDTO> Create a new persona. name is required.
update(personaId, input) Promise<PersonaDTO> Update a persona. All fields are optional.
delete(personaId) Promise<boolean> Delete a persona. Returns true if deleted.
switchActive(personaId) Promise<void> Switch the active persona. Pass null to deactivate.
getWorldBook(personaId) Promise<WorldBookDTO \| null> Get the world book attached to a persona. Returns null if none attached.

PersonaDTO

{
  id: string
  name: string
  title: string                         // short tagline
  description: string
  image_id: string | null               // avatar via Images API
  attached_world_book_id: string | null // linked world book
  folder: string                        // organizational grouping
  is_default: boolean
  metadata: Record<string, unknown>
  created_at: number                    // unix epoch seconds
  updated_at: number
}

PersonaCreateDTO

Field Type Required Description
name string Yes Persona name
title string No Short description/tagline
description string No Full persona description
folder string No Organizational folder label
is_default boolean No Set as the default persona (clears previous default)
attached_world_book_id string No World book ID to attach
metadata Record<string, unknown> No Custom metadata

PersonaUpdateDTO

Same fields as PersonaCreateDTO, but all are optional (including name).

Active Persona

getActive() reads the activePersonaId setting that the frontend persists when the user selects a persona. switchActive() writes to the same setting and emits a SETTINGS_UPDATED event, so the frontend updates immediately.

// Contextual persona switching
const active = await spindle.personas.getActive()
if (active?.folder === 'Roleplay') {
  spindle.log.info('User is in roleplay mode')
}

// Switch and notify
await spindle.personas.switchActive('new-persona-id')
spindle.toast.success('Persona switched!')

Attached World Books

Each persona can have one world book attached via attached_world_book_id. The getWorldBook() method is a convenience that reads the persona, checks the attachment, and returns the full WorldBookDTO — all in a single call. Only personas permission is required (not world_books).

const persona = await spindle.personas.getActive()
if (persona) {
  const wb = await spindle.personas.getWorldBook(persona.id)
  if (wb) {
    spindle.log.info(`Persona "${persona.name}" uses world book "${wb.name}"`)
  }
}

Note

For user-scoped extensions, the user context is inferred automatically. For operator-scoped extensions, the user ID is resolved from the extension context. Personas are always scoped to a single user.