Character authoring
POST /v1/characters can accept only slug and display_name, but that is a
smoke test, not a production character. A production character should give
LoreOS enough typed state to answer three questions:
- Who is this character?
- How do they talk?
- What kind of life do they live between messages?
LoreOS stores these fields as versioned character content, validates the shape,
returns authoring_readiness, emits the authored greeting when a session starts,
passes deeper authoring fields into runtime chat as private planning context, and
drives the character’s between-message life (Story Room weekly arcs and daily-life
packages read these fields today — see What happens at runtime).
Create, validate, list, get, and update responses also return publication.
Use it to separate runtime availability from launch quality:
publication.runtime_available=truemeans/v1/sessionscan use the character.publication.authoring_quality_ready=truemeans the authored character passed the readiness checklist.publication.launch_ready=truemeans both runtime availability and authoring quality are ready.
The common early state is publication.state="needs_attention" with
runtime_available=true. That is a successful smoke-test character, not a character
you should show to real end users yet.
POST /v1/characters auto-publishes — there is no separate publish endpoint. A created
character is published immediately, so publication.runtime_available=true on create
regardless of readiness, and /v1/sessions can use it right away. The readiness
checklist (authoring_readiness / publication.authoring_quality_ready) is advisory, not
a gate: a needs_attention character is fully runtime-available; readiness only tells you
whether it is good enough to show real users. Use publication.launch_ready=true — not
runtime_available — as your own ship gate.
Agent-safe flow: call POST /v1/characters/validate, read
authoring_readiness.next_authoring_actions, create or patch the character, then
use publication.launch_ready=true as the gate for showing it to real end users.
GET /v1/characters/{slug}/readiness is a broader launch checklist and may still
ask for visual identity, inventory, budget, or delivery work even after
authoring_readiness.status="ready".
Before seeding a roster, call GET /v1/characters/authoring-limits. It returns
bulk_create_max_per_request, demo sandbox caps when relevant, readiness targets,
and the publication-state glossary.
Authoring quota
POST /v1/characters/validate dry-runs do not consume quota (no write, no character
count increment) — so you can validate as many drafts as you like before spending a create.
Character lifecycle
Use POST /v1/characters/{slug}/archive when you want to remove a character from active
inventory without losing the canonical character id or slug history. Archived characters
are hidden from the default GET /v1/characters list and cannot start sessions, receive
new messages on existing sessions, or activate background features.
Restore with POST /v1/characters/{slug}/restore.
Use DELETE /v1/characters/{slug}?confirm_slug={canonical_slug} only after archiving when
the character should be permanently removed from product runtime surfaces. Delete returns
202 with a deletion id and a status URL; poll GET /v1/characters/{slug}/deletion until
the purge completes. Security, billing, and lifecycle audit ledgers follow their own
retention rules.
POST /v1/characters/{slug}/block is not character deletion. It blocks one end-user’s
relationship with that character and should not be used for inventory cleanup.
The drive-to-ready loop
To climb a character to launch quality without burning quota on failed creates:
POST /v1/characters/validate— dry-run scoring, no quota cost.- Read
authoring_readiness.next_authoring_actionsfor the exact missing fields. POST /v1/characters(first time) orPATCH /v1/characters/{slug}(subsequent) to apply the fix.GET /v1/characters/{slug}/readinessto re-check.- Repeat 2–4 until
authoring_readiness.status="ready"(and, for the broader launch checklist, untilGET /v1/characters/{slug}/readinessis satisfied).
Minimal vs. production-quality
Minimal characters are allowed so agents can smoke-test the API quickly:
That character can chat, but it will feel generic. For a character users should care about, include the fields below.
Field guide
Reply language and input locale guard
primary_reply_language is the language users should normally see in character replies for
a single-locale character. Choose it from the product experience you are shipping, not from
where the character lives, which language the developer speaks, or the language of the latest
user message.
locale is separate metadata for regional hints, provider behavior, formatting, and legacy
compatibility. A character can have locale: "ko-KR" and primary_reply_language: "en-US"
if the character is culturally Korean but your product wants English replies.
All four language fields (locale, primary_reply_language, supported_input_languages,
input_language_policy) are also readable: they are returned by GET /v1/characters
(list) and GET /v1/characters/{slug} (one), so your app can read them per character and do
its own client-side exposure or input-blocking without maintaining a separate
character→language mapping.
supported_input_languages is the runtime guard for POST /v1/sessions.locale.
If a character is KO-only, leave it as ["ko-KR"]; a session request with
"locale":"en-US" returns 409 session_locale_not_supported. Add languages only
when the character is intentionally authored to handle them, or set
input_language_policy="allow_cross_language" when your app owns catalog routing.
For multilingual characters, create one canonical character and add locale packs:
Start a Korean session with POST /v1/sessions {"character":"jenny","locale":"ko-KR",...}.
That session sees only the Korean greeting, voice samples, example dialogues, localized
profile, and reply language. Other locale packs are not injected into the runtime context.
Manage packs after creation with GET /v1/characters/{slug}/localizations and
PATCH /v1/characters/{slug}/localizations/{locale}.
If a developer-facing slug needs to change, use
POST /v1/characters/{slug}/rename {"slug":"new-slug"}. character_id remains the stable
identity and the old slug stays as an app-scoped alias.
The runtime may understand multilingual user input, quoted text, names, URLs, or source
material, but it does not switch the character’s reply language just because the latest
user message was in another language. If you want stable code-switching, author it
deliberately in voice_samples and example_dialogues, but still set one primary reply
language as the default visible language.
Re-publishing a single-locale slug with a different primary_reply_language returns
409 primary_reply_language_fixed. For multilingual support, add localizations instead
of creating duplicate language-specific characters.
North star: aspiration, life direction, and drama dials
The single biggest lever against a flat, aimless character is giving it a direction:
a conscious dream it is moving toward and dials that shape how its off-chat life evolves.
These keys ride inside profile (the profile object accepts extra keys — see
Extra profile keys) and are consumed at
runtime by the life-arc writer to channel the character’s evolving life toward that goal.
Note.
aspiration.summary,name, androleare required where shown; everything else is optional. All of these are vertical-agnostic — a tutor, companion, or any other character can carry a life direction, not just dating personas. Because they ride onprofile, aPATCHthat sends a newprofilereplaces it wholesale: include the full profile (these keys plus your visible fields) to change one of them.
Behavioral thresholds: enabling selfies and proactive messages
behavioral_thresholds is the only way to turn on two headline behaviors that are off by
default: the character sending selfies, and the character messaging first. Pass it as a
flat object on POST/PATCH; omitted keys keep the engine defaults, and a PATCH
deep-merges (you only send the keys you want to change).
Images (all default 0 = no images). Selfies stay off until you raise these and
upload an identity image (POST /v1/characters/{slug}/visual-assets):
Proactive messages. Whether the character can start or resume a conversation:
Emission and exit (advanced — how the character paces and withdraws):
withhold_propensity (float, default 0.4), emit_delay_when_low_affection (bool,
default true), withdrawal_phrasing_style ("soft" | "direct" | "cold"),
exit_boundary_violation_count (int 1..10, default 3), and re_engage_openness
(float, default 0.5). Leave these at defaults unless you are deliberately tuning tone.
Per-session override:
POST /v1/sessionsacceptsproactive_enabledat creation, andPATCH /v1/sessions/{id}/proactive-controlsacceptsproactive_enabledfor existing sessions, without changing the character.
Affordance schema: the relational domain
affordance_schema declares which relationship the engine is tracking. It defaults to
the dating domain; a built-in tutor preset swaps the dating-flavored dimensions
(affection, romantic interest, trust) for learning-flavored ones (rapport, mastery
confidence, frustration). You can pass:
- a preset name —
"dating"(default) or"tutor"; - a
{ "preset": "tutor" }pointer (equivalent); - a full inline schema for a custom domain.
Only
datingandtutorare built-in presets. There is nocompanionpreset — acompanion(or any other) domain must be passed as a full inline schema (aschema_version, adomainlabel, and a list ofdimensions, each withkey,prompt_rendering_name, and optionaldefault/range/lower_is_open). Passing an unknown preset name is rejected with422.GET /v1/characters/{slug}/runtime-previewshows the declared dimension names the engine will track.
Safety and forbidden boundaries
LoreOS spreads “what this character must never do” across a few typed surfaces. Set all of the relevant ones — they are read at different stages of generation and reply:
forbidden_style and handoff_triggers are typed list[dict] — each entry must be an
object (a list of plain strings is a 422). The schema accepts any keys, but for
forbidden_style only expression / pattern_description / anchor_tier are actually read
at runtime (arbitrary keys like rule/severity are stored but ignored). The exact accepted
shape, the consumed keys, and the string-vs-object 422 are worked through in
Recipe → forbidden_style.
Set these together with must_not/blocked_motifs/boundaries so safety is consistent
across voice, story, and relationship.
Extra profile keys that ARE consumed at runtime
profile accepts extra keys for forward compatibility. This is a footgun: an unknown
profile key is silently preserved but never read, and you only get a soft warnings entry
(profile_unknown_keys) — not a hard error. A typo like visible_intrests becomes dead
state.
The profile keys that are not in the visible-profile schema but are consumed at runtime (so they are expected, not typos) are the north-star keys:
aspirationlife_directiondrama_intensityconcreteness_floorarc_appetitesupporting_cast
Any other extra profile key is treated as dead state. Check the authoring_readiness.warnings
for profile_unknown_keys after every create/update to catch typos early.
Readiness
Every create/get/update response includes authoring_readiness:
needs_attention does not block the API. It tells you the character will likely
feel flat unless you add more authoring depth. Coding agents should treat
missing_high_impact_fields and next_authoring_actions as a checklist before
shipping.
Publication state
publication is returned on create, validate, list, get, and update responses:
State meanings:
Agents should use launch_ready, not runtime_available, to decide whether a
character can be shown to real end users.
For POST /v1/characters/bulk, use results[].status only for per-item
transport outcome: ok means the item created or republished, error means the
item failed while other items could still succeed. Successful items include
character_status and publication for runtime/publish readiness. Do not treat
results[].status as the character publish state.
How readiness is scored
authoring_readiness is computed from fourteen presence checks, and the rule is exact:
authoring_quality_score= the percentage of the fourteen checks that are present (so each check is worth ~7 points; e.g. 13/14 present rounds to93).- The fourteen checks are:
profile.visible_bio,profile.visible_interests,profile.identity_anchors,voice_samples >= 3,example_dialogues >= 2,greeting,onboarding,character_direction,life_template,story_engine,world_model_seed,arc_seeds,relationship_seed,emotion_baseline. statusis"ready"only when the score is>= 85and there are zerowarnings. Otherwise it is"needs_attention".
voice_samples and example_dialogues are deliberately separate. A voice sample is a single
line that teaches phrase shape. An example dialogue is a short exchange that teaches how the
character handles user pressure, curiosity, flirting, ordinary check-ins, and topic pivots.
Runtime treats example dialogues as private style-and-pacing guidance, not as events that
happened in canon.
Four warnings can hold you at needs_attention even with a decent score:
needs_loreos_depth— fires if any of the seven “depth” fields is missing (character_direction,life_template,story_engine,world_model_seed,arc_seeds,relationship_seed,emotion_baseline). This is the warning that makes “ready” require real depth, not just a high score.voice_samples_few— fires with fewer than 3 voice samples.example_dialogues_few— fires with fewer than 2 user↔character example exchanges.profile_unknown_keys— fires on a profile key the engine doesn’t recognize (a typo). The north-star keys do not trip it (see Extra profile keys).
The scorer only ever returns "ready" or "needs_attention". The third documented status,
"invalid", means the body was rejected by schema validation before scoring — i.e. you
got a 422 (invalid_character / validation_error) and no readiness object at all, not a
scored "invalid".
Three tiers, and the status each one returns
Use these as the rungs to climb. The status for each is exactly what the scorer returns.
Tier 1 — smoke-test minimum → needs_attention (score 0).
The least the API accepts. Lets an agent prove the create/session/message loop, nothing more.
Returns status: "needs_attention", authoring_quality_score: 0, warnings
["needs_loreos_depth", "voice_samples_few", "example_dialogues_few"], and all fourteen checks in
missing_high_impact_fields. (Adding just a greeting nudges the score to 7 — still
needs_attention.) This character can chat but will feel generic.
Tier 2 — good chat character → still needs_attention (score ~46).
Enough to feel authored in conversation — persona, voice, an authored first message, and a direction — but not yet the full between-message life.
Returns status: "needs_attention" with authoring_quality_score around 50 and a single
needs_loreos_depth warning — because the six remaining depth fields (life_template,
story_engine, world_model_seed, arc_seeds, relationship_seed, emotion_baseline) are
still missing. A character at this tier chats well but its off-chat life stays thin.
Tier 3 — production character → ready (score up to 100).
Every block filled. This is the level the platform’s life/Story-Room engine needs to give the character a real, directed life between messages.
Fill all fourteen checks (a complete, copy-paste body is in Recipe: a production-grade dating character). With all fourteen present and no warnings, the response is:
onboarding is the one check you can omit and still reach ready: with the other thirteen
present (all six depth fields, 3+ voice samples, 2+ example dialogues, greeting, and the
three profile keys) the score is 93 and there are no warnings, so status is ready.
Add onboarding anyway — it’s private self-knowledge the character draws on for planning —
but know it isn’t the gate.
Climbing from needs_attention to ready
- Add the three
profilekeys (visible_bio,visible_interests,identity_anchors). - Add 3 or more
voice_samples(each withtext) → clearsvoice_samples_few. - Add 2 or more
example_dialogues(each with bothuserandcharacterturns) → clearsexample_dialogues_few. - Add a
greetingso the first message is authored. - Add all seven depth fields —
character_direction,life_template,story_engine,world_model_seed,arc_seeds,relationship_seed,emotion_baseline→ clearsneeds_loreos_depth(a partial set does not clear it). - Re-run
POST /v1/characters/validate(a dry run; same scoring, no write) untilwarnings: []andauthoring_quality_score >= 85. Watch forprofile_unknown_keys— it means a profile key is misspelled.
What happens at runtime
When you start a session:
- LoreOS emits
greeting.bubblesascharacter.initiated. - Runtime chat receives profile, voice, and deeper authoring fields as private planning context.
- The model may use that context for tone, pacing, life rhythm, and plausible daily details.
- The model may not quote or reveal private authoring context as if it were a committed event, user consent, or future plan.
Story Room consumes these fields directly today. life_template feeds daily-life
planning, story_engine feeds recurring event sources and supporting cast,
arc_seeds feed soft weekly direction, and character_direction,
relationship_seed, and emotion_baseline shape the tone and constraints of the
generated life. The north-star keys (aspiration / life_direction / the dials)
channel where that life is heading. These are not “planning-only” placeholders —
they drive the weekly and daily Story Room generation that the runtime then speaks
from.
Good authoring checklist
primary_reply_languageis explicit and matches the language users should see.- Multilingual characters use
localizations; they are not duplicated per language. profile.visible_bionames a concrete life, not “a friendly person.”profile.visible_interestshas at least three specific interests.profile.identity_anchorshas stable identity/style anchors.voice_sampleshas at least three lines, ideally five or more, in the same language asprimary_reply_languageunless code-switching is intentionally authored.example_dialogueshas at least two compact user↔character exchanges: one ordinary daily-share/check-in and one pressure/boundary/pivot case.greetingis specific enough to feel authored.character_direction.promisesays what kind of character experience you are promising.life_templateincludes real day blocks: work, meals, hobbies, obligations, social contact, exercise, recovery, or rest.story_engine.allowed_event_sourcesincludes low-risk friction and social contact sources.relationship_seedandemotion_baselineset the starting posture without forcing the user.profile.aspiration+life_directiongive the character a direction so its off-chat life is not flat or aimless.behavioral_thresholdsare set deliberately if you want selfies (raiseimage_proactivity/daily_image_capand upload an identity image) or proactive messages (cold_initiate_enabled/daily_initiate_cap).affordance_schemamatches the relationship you are building (datingdefault,tutorpreset, or a full inline schema for any other domain).- Safety boundaries are set across
must_not,blocked_motifs,boundaries,forbidden_style, andhandoff_triggers.
Common mistakes
- Assuming
localecontrols speech. Useprimary_reply_languagefor visible reply language on single-locale characters, orlocalizations[locale].reply_languagefor multilingual characters. - Duplicating the same character per language. Use one character with locale packs.
- Mixing English, Korean, Japanese, or Chinese voice samples accidentally inside one locale pack. Mixed-language anchors should be intentional.
- Putting
character_directioninsideprofile. It is a top-level field. - Sending
voice_sampleswithouttext. That is a validation error. - Treating
arc_seedsas a script. They are soft directions and must allow user intervention. - Using only visual texture. A character also needs obligations, relationships, and ordinary activities.
- Putting secret future plans in speakable fields like
visible_bio.