Skip to main content

Scenarios

A scenario puts a persona in a situation with an objective and says how the call is graded.

List scenarios​

GET /api/nn-coach/scenarios — any member

Create a scenario​

POST /api/nn-coach/scenarios — manager or above

{
"persona_uuid": "pe1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8",
"name": "Price objection — renewal",
"situation": "You are calling an existing customer whose contract ends in March.",
"objective": "Book a renewal meeting with the owner.",
"difficulty": "hard",
"category": "renewals",
"agent_opens": true,
"hidden_state": { "competitor_quote": 38000 },
"stop_conditions": {
"max_turns": 20,
"satisfied_when": "the customer agrees to a meeting date",
"hangup_triggers": ["rep becomes argumentative", "rep repeats the same pitch three times"]
},
"rubric": "SPIN"
}
FieldRequiredNotes
persona_uuid✓
name✓
situation✓The setup, told to the simulated customer
objective—What the rep is supposed to achieve
difficulty—easy, medium, hard, or adversarial — enforced by a database constraint
category—Grouping
agent_opens—Who speaks first. Absent means true (outbound)
hidden_state—What this customer withholds on this call
stop_conditions—When the call ends
rubric—Rubric name — mutually exclusive with rubric_field_ids
rubric_field_ids—Explicit criteria instead of a named rubric
rubric_overrides—The same thing as rubric_field_ids, under the name a scenario reads back as
StatusCause
400{"detail": "persona_uuid, name and situation are required"}
404{"detail": "persona not found"}
agent_opens is a pointer, and absence means outbound

An omitted agent_opens means true, not Go's zero value. Every scenario would otherwise be inbound, which is what happened before the field existed.

Hidden state, on top of the persona's​

The persona's hidden_state is what that customer always keeps back. The scenario's is what they withhold on this call.

Leave it out for a drill where discovery is not the point, so the judge does not penalise a rep for failing to qualify a customer nobody asked them to qualify.

Stop conditions​

{
"max_turns": 20,
"satisfied_when": "the customer agrees to a meeting date",
"hangup_triggers": ["rep becomes argumentative"]
}
FieldEffect
max_turnsHard ceiling. Unset falls back to the service default
satisfied_whenThe objective is met and the call can end successfully
hangup_triggersThe customer walks away

hangup_triggers is what makes a practice call feel real. A customer who cannot leave is one a rep can bore into submission, and that is not a skill worth rehearsing.

max_turns lives on the scenario rather than the run: how long a conversation should go is a property of the drill.

Rubrics​

GET /api/nn-coach/rubrics lists what is available, grouped by category:

{
"results": [
{
"category": "Consultative Selling (FAB)",
"fields": [
{
"id": "fab_features_accurate",
"name": "Features stated accurately",
"field_type": "score",
"description": "Were the product features described correctly?"
}
]
}
]
}

Send the category as rubric, or individual id values as rubric_field_ids.

Send either:

  • rubric — a methodology name such as "SPIN" or "Language Fluency". This keeps the client out of the business of knowing what a methodology contains.
  • rubric_field_ids — an explicit set of criteria, for something assembled by hand.

They are mutually exclusive; sending both is a 400.

rubric_overrides accepts the same value as rubric_field_ids. It exists so a client can round-trip a scenario: GET returns the resolved ids under rubric_overrides, and sending that object straight back would otherwise be rejected for using a field name it had just been given. Sending both spellings with different values is an error.

Delete a scenario​

DELETE /api/nn-coach/scenarios/{uuid} — manager or above