Recipe: a production-grade dating character
Recipe: a production-grade dating character
A complete, copy-paste body that fills every authoring block, plus the validation traps that bite.
This page is a single, copy-paste-ready POST /v1/characters body for a dating
character that fills every authoring block LoreOS reads — profile (with a north-star
aspiration), voice samples, greeting, onboarding, direction, life template, story engine
with supporting cast, world seeds, arc seeds, relationship seed, emotion baseline,
affordance_schema, and behavioral_thresholds. Every field below is validated against the
typed authoring contract — there are no invented keys.
If you only want the field-by-field reference, read Character authoring.
This page is the worked example plus a valid-vs-invalid section for the handful of fields
that produce a real 422.
Send this body to POST /v1/characters/validate first (a dry run — same validation, no
character created). When it comes back clean, the real POST /v1/characters is guaranteed
to validate. See Validate before create.
The full body
What “ready” means for this body
The create/get response carries authoring_readiness. This body fills all thirteen
scored checks (profile.visible_bio, profile.visible_interests,
profile.identity_anchors, voice_samples >= 3, greeting, onboarding,
character_direction, life_template, story_engine, world_model_seed, arc_seeds,
relationship_seed, emotion_baseline), so:
This body sets the north-star keys (aspiration, life_direction, drama_intensity,
concreteness_floor, arc_appetite, supporting_cast) inside profile, and that is fully
recognized: the readiness scorer knows the north-star keys, so they do not trip a warning.
With the score high and zero warnings, this body returns status: "ready".
status is only "ready" when the score is high and there are no warnings. The one warning
to watch for here is profile_unknown_keys, which now fires only on a genuinely unknown key
— a typo like visible_intrests instead of visible_interests, which is silently preserved
(via extra="allow") but never read at runtime. Treat profile_unknown_keys as a prompt to
look: it means a profile key you wrote isn’t one the engine knows, so fix the spelling and
re-validate.
The leverage each block adds, and the runtime behaviors it drives, are documented in Character authoring and Story Room and life. To turn this into a runnable end-to-end project, see Deploy a Node chat.
Validate before create
POST /v1/characters/validate is a dry run: it runs the exact same validation and
readiness scoring as POST /v1/characters (north-star shape, affordance schema, every field
range) but never persists anything. A clean validate guarantees a clean create, so an agent
can iterate to missing_high_impact_fields: [] before spending a create.
validate uses the same 422 codes as create. If it fails, the response body is the
standard error envelope — fix the field it names and re-validate:
Valid vs. invalid: the fields that produce a 422
Most authoring fields are forgiving. These six are where real requests fail, with the exact
error each one returns. The error body is always
{ "detail": { "code": "...", "message": "...", "fix": "..." } }; the loc/msg shown is
from the underlying validation error quoted back in message.
forbidden_style — list of objects, never list of strings
This is the single most common 422. forbidden_style is a list[dict] (each entry is an
object describing one style rule). Passing a list of plain strings fails.
The keys the runtime actually reads from each object are:
-
expression— an exact literal phrase to avoid, or -
pattern_description— a description of the phrasing/cadence to avoid.The engine uses
expressionif present, otherwisepattern_description— so each entry needs at least one of the two to carry any meaning. An entry with neither is silently dropped (it never reaches the critic), so it won’t422but also won’t do anything. -
anchor_tier—"T1"–"T3", strongest first; defaults to"T1"when omitted.
The object is otherwise free-form: extra keys are preserved on the record but are not
read. In particular, an object like { "rule": "...", "severity": "hard" } validates fine
(it’s a dict) yet has no effect, because the runtime reads expression/pattern_description,
not rule/severity. Use pattern_description (or expression). handoff_triggers has the
same list[dict] shape and the same “must be an object” trap.
Today forbidden_style is strictly list[dict], so a list of plain strings is a 422.
Accepting a bare string as shorthand for { "pattern_description": "<the string>" } would be
a nice ergonomic improvement, but it is a server-side change and is not supported yet —
for now, always send objects.
world_model_seed — typed entries (or the legacy domain/claim_text pair)
Each seed is either a typed entry (type + name + description) or a legacy
claim (domain + claim_text). A typed entry auto-derives the legacy domain/claim_text
for you; a seed that supplies neither a usable name/description nor a claim_text is
silently dropped at session time (never speakable), so the trap here is a seed that does
nothing, not a hard 422.
type is one of location | person | object | organization | routine | constraint | claim
(default claim). visibility is character_known | session_visible | developer_only
(default character_known) — developer_only seeds are never speakable. salience is
0..1. Prefer the typed form: it is self-documenting and the engine fills the legacy fields.
arc_seeds — allowed_movements is a fixed enum
allowed_movements only accepts plant, reinforce, partial_payoff, defer, abandon.
Anything else (escalate, resolve, climax, …) is a 422.
title and arc_question are both required on every arc seed. Arc seeds are soft
directions, not a script — keep forbidden_outcomes and user_agency_points so the user can
intervene.
onboarding — both question and answer are required
Each onboarding entry needs a non-empty question and a non-empty answer. Sending one
without the other is a 422.
privacy_tier (default "private_knowledge") and allowed_uses are optional. Onboarding is
private self-knowledge the character can use for planning — it is not spoken verbatim.
behavioral_thresholds — 0..1 floats and 0..N ints, and 0 means OFF
Image and proactive knobs are bounded. A float over 1.0 (or an int over its ceiling) is a
422. Just as important: the image knobs default to 0, which means images are OFF —
leaving them at 0 is valid but the character will never send a selfie.
To actually get selfies you need both image_proactivity/selfie_comfort > 0 and
daily_image_cap > 0 and an uploaded identity image — see
Visual identity.
The recipe above authors profile.identity_anchors as a list[str] (it doubles as
voice/persona anchors). The contract accepts both shapes — identity_anchors is typed
list[str] | dict, so a dict is settable straight through POST /v1/characters with no 422
— but the image pipeline reads only the dict form of visual keys. So if you turn images
on (as this recipe does), author profile.identity_anchors as an object (visual_summary /
hair / style_constants …) instead; a dict satisfies authoring readiness and selfie
generation, while a list satisfies readiness but leaves the image pipeline without a visual
reference. Full explanation:
Visual identity → the dict requirement.
daily_image_cap is 0..20,
daily_initiate_cap is 0..50; cold_initiate_enabled defaults to false and
session_resume_enabled defaults to true. The full knob list is in
Character authoring.
affordance_schema — only dating and tutor are presets
affordance_schema declares the relationship the engine tracks. The only built-in preset
names are dating (the default) and tutor. There is no companion preset — any
other domain must be a full inline schema. An unknown preset name is a 422.
Each inline dimension needs a key (^[a-z][a-z0-9_]*$) and a prompt_rendering_name;
default (in range, default 0.3), range (default [0, 1]), and lower_is_open
(default false) are optional, and dimension keys must be unique.
GET /v1/characters/{slug}/runtime-preview shows the declared dimension names the engine
ends up tracking. See Character authoring.
Checklist before you ship this character
validatereturnsvalid: true,missing_high_impact_fields: [], and (for this body)status: "ready"with nowarnings.- No
profile_unknown_keyswarning. The north-star keys (aspiration,life_direction, …) are recognized and don’t warn; this warning now fires only on a genuinely unknown key — a typo likevisible_intrests. If you see it, fix the spelling. forbidden_styleandhandoff_triggersare lists of objects, not strings.- Every
arc_seeds[].allowed_movementsvalue is one of the five allowed movements, and each arc seed has atitleandarc_question. - Each
onboardingentry has bothquestionandanswer. behavioral_thresholdsfloats are0..1and ints are within range; image knobs are> 0only if you also uploaded an identity image.affordance_schemaisdating/tutoror a full inline schema with unique dimension keys.