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"
}
| Field | Required | Notes |
|---|---|---|
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 |
| Status | Cause |
|---|---|
400 | {"detail": "persona_uuid, name and situation are required"} |
404 | {"detail": "persona not found"} |
agent_opens is a pointer, and absence means outboundAn 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"]
}
| Field | Effect |
|---|---|
max_turns | Hard ceiling. Unset falls back to the service default |
satisfied_when | The objective is met and the call can end successfully |
hangup_triggers | The 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