For AIs, authors & curious humans
The Training Sim
blueprint.
The complete schema for the blueprint JSON: every step type, every field, every default and every rule the importer enforces. Hand this document to an AI or point it at /blueprint.md, describe your training, then paste the JSON it produces into the Training Sim Studio.
# The Training Sim Blueprint (v1)
A blueprint is a single JSON object that describes a whole interactive
training or presentation: the chat conversation the viewer watches AND
the decisions they make. You can write one yourself, but the usual flow
is to hand this document to an AI, describe what you want, and paste or
upload the JSON it produces into ChatAnimate's Training Sim Studio
(Interactions → Import blueprint).
Everything below is the exact contract the importer enforces — written
from the runtime itself, not from aspiration. Where the platform fixes
something for you automatically, it says so. Where something is not
possible, it says that too, plainly, so neither you nor the AI wastes
time inventing features that do not exist.
---
## How this works
1. Give this document to any AI (or point it at `/blueprint.md`), then
describe your training after it.
2. The AI may ask you a few questions first — who holds the phone,
whether answers should branch the story, whether it is scored. Answer
them; they are the questions whose answers change the JSON.
3. Paste the JSON it produces into the Import tab (or POST it to the
publishing API). ChatAnimate validates it, tells you everything it
had to fix, and applies it.
4. Export your current draft any time in the same format: the next AI
round starts from everything that already exists. If the AI's answer
fails validation, paste the error lines back to it together with
your exported blueprint and ask it to fix them.
A blueprint with only `messages` imports a conversation with no
interactions. A blueprint with only `steps` adds interactions to the
conversation that is already open in the editor.
---
## The prompt to give the AI
Copy everything between the lines into your AI chat, then describe your
training. The AI may ask you a few questions first (who holds the
phone, whether answers branch the story, scoring, media) — answer them
and it will build; when the description is already complete it builds
straight away.
```
You build ChatAnimate interactive blueprints: a training or presentation delivered as a chat conversation the viewer watches, with questions, media, cards and branches that pause it.
STEP 0 — THE INTERVIEW (ask before you build)
The description you are given may leave material facts open. Guessing them is the #1 way blueprints go wrong, so ask first: reply with ONLY the numbered questions (at most five, one line each, nothing else) and wait for the answers. Ask only questions whose answers change the JSON. The five that usually matter:
1. Who holds the phone? Whose chat is this — and is the viewer supposed to BE the right-side person making the decisions, or watch two other people talk?
2. One path or decisions? Should the conversation simply play to the end, or do answers change what the chat says (branches, different endings)?
3. Scoring? A quiz with points and a pass mark, an ungraded experience, or a presentation?
4. Media? Any video to watch, PDF to read or picture to tap — and are the files ready as public URLs, or should the author upload them after import?
5. Tone and length? Serious or playful, and roughly how long should the conversation run?
When the description already answers a question, do not ask it again. When everything material is answered (or the description is clearly a creative brief to run with), build immediately. From that point your reply is the blueprint alone: return ONLY one valid JSON object. No prose before or after. No markdown code fences.
THE SIDES (who is who — read this before writing a single message)
The chat has exactly two sides:
- "right" is the SENDER: the owner of the phone, "you", the person the viewer identifies with (the trainee, the employee, the protagonist).
- "left" is the OTHER person: the contact the chat is with (the colleague, the manager, the scammer).
A message's "from" says who speaks. It accepts, in this order of preference:
- "left" or "right" — the canonical values. Prefer these.
- "sender", "me", "you" or "self" — aliases for right (the phone's owner).
- "receiver", "them", "other" or "contact" — aliases for left (the other person).
- a participant NAME — when you declare participants (below), "from": "Maya" means Maya, on the side participants gives her.
- omitted — the sides alternate (message 1 is left, then right, then left...).
ALWAYS write an explicit "from" on every message. Omitted values alternate, which is a fallback, not a style: when the same person sends two messages in a row, alternation puts the second one on the WRONG side. A thought is always the RIGHT-side person's inner monologue; never write a thought for the left-side character.
PARTICIPANTS (the people, optional)
[{ "name": "Maya", "side": "left" }, { "name": "Daniel", "side": "right" }]
- "name" (required, unique) is the exact string a message's "from" references (matching ignores case).
- "side" is "left" or "right". When omitted: the first participant is the left-side contact, the second the right-side sender, everyone else a left-side group member.
- Declare participants whenever the story names its people: the import applies the names to the conversation, and every "from" can then use them.
GROUP CHATS (a conversation with more than two people)
Declare 3 to 8 participants and the conversation becomes a GROUP (a Slack channel, a team chat, a family group). One participant is on the right (the phone's owner, "you"); everyone else is a left-side member with their own name, and every message's "from" uses the speaker's name so the viewer always sees who said what:
{ "participants": [ { "name": "Sam", "side": "right" }, { "name": "Maya" }, { "name": "Priya" }, { "name": "Daniel" } ], "groupName": "#security-team", "messages": [ { "from": "Maya", "text": "Did anyone else get that invoice?" }, { "from": "Sam", "text": "Just now. Looks off." } ] }
- "groupName" (optional, max 60 characters) is the group's title — the chat header the viewer sees. Omit it and the title is "Group Chat".
- A GROUP is still ONE conversation. Do not split one group across contexts; a context is its own separate conversation (a group context declares its own "participants" the same way).
- Keep groups readable: 3 to 5 participants is the sweet spot. More than 8 is rejected.
THE SCHEMA
The rest of this document IS the schema: the overall shape, every step type and field, how routing works, branches, contexts, calls, consequences, scoring, settings, media placeholders, what is not possible, what the importer fixes automatically, the rules the import enforces, and the validation checklist. Follow it exactly; never invent fields, step types or behaviors it does not describe.
```
The studio's Ask-an-AI tab copies the complete machine version of this
prompt (every field, every rule, the full validation checklist) from
the same module the importer uses, so it always matches the real
schema.
---
## The mental model
A blueprint has two layers that never mix:
- **`messages` is a POOL, not a flow.** It holds every line of every
conversation: the main chat plus any additional conversations
(contexts). Nothing plays just because it is in the pool. What plays
is decided by `playback` steps — and when there are none, the whole
main conversation plays start to finish.
- **`steps` run in order, top to bottom.** Cards (question, feedback,
info, video, document) appear at message positions. Logic steps
(jump, score, delay, variable, condition, playback, completion) run
invisibly between them. A `goTo` on any step changes where the flow
goes next; answers carry their own `goTo` too.
The viewer watches a phone. The conversation types itself out; when a
card's position is crossed, the conversation pauses and the card opens;
when it is answered or dismissed, the conversation resumes. The final
screen is a `completion` step: score, pass result, certificate, retake.
**Two runtime behaviors worth knowing before you design (they are
design constraints, not bugs):**
- While the flow stands on a card, only THAT card opens. Sibling cards
anchored after the same message (a question and its per-answer
coaching cards) do not hijack each other — the answer's route decides
which one the viewer sees.
- A question opens at most once per run. If a playback range replays a
slice that re-crosses an already-answered question, the question does
not open again (re-answering follows `settings.reanswer`). Design
branches accordingly: never rely on "re-asking" a question later.
---
## The overall shape
```json
{
"version": 1,
"title": "The Art of Saying No",
"participants": [],
"contexts": [],
"messages": [],
"steps": [],
"settings": {}
}
```
| Field | What it means | Required |
| -------------- | ---------------------------------------------------------------------------------------- | ----------------------- |
| `version` | Always `1`. | yes |
| `title` | A working title. The author can change it later. | no |
| `participants` | The people in the chat — two for a 1:1, 3 to 8 for a group chat (see below). | no |
| `groupName` | The group chat's title, when `participants` makes the chat a group (max 60 characters). | no |
| `contexts` | The other conversations the scenario can flow into (the consequence pattern, see below). | no |
| `messages` | The conversation script: what plays on the phone. | one of messages / steps |
| `steps` | The decisions: questions, videos, documents, branches. | one of messages / steps |
| `settings` | Pass score, retake behavior, certificate, message speed. | no |
---
## participants (the people)
```json
[
{ "name": "Maya", "side": "left" },
{ "name": "Daniel", "side": "right" }
]
```
| Field | What it means | Required |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `name` | The exact string a message's `from` may reference (matching ignores case). Unique. | yes |
| `side` | `"left"` or `"right"`. When omitted: the first participant is the left-side contact, the second the right-side sender, everyone else a left-side group member. | no |
On import the names are applied to the conversation: the right-side
name becomes the sender ("you", the phone's owner), the left-side name
becomes the main conversation's contact. A context's own `senderName`
still wins for named conversations. Before you apply, the import's
verdict shows the mapping — who lands on which side — so a mixed-up
pair is visible before anything changes.
Declare participants whenever the story names its people. It is the
difference between `"from": "Maya"` meaning Maya and `"from": "Maya"`
silently flipping sides.
### Group chats (more than two people)
Declare 3 to 8 participants and the conversation becomes a **group
chat** — exactly what the engine's group chats do (a Slack channel, a
team chat, a family group). One participant is on the right (the
phone's owner, "you"); everyone else is a left-side member with their
own name, and every message's `from` uses the speaker's name so the
viewer always sees who said what:
```json
{
"participants": [
{ "name": "Sam", "side": "right" },
{ "name": "Maya" },
{ "name": "Priya" },
{ "name": "Daniel" }
],
"groupName": "#security-team",
"messages": [
{ "from": "Maya", "text": "Did anyone else get that invoice?" },
{ "from": "Sam", "text": "Just now. Looks off." }
]
}
```
`groupName` (optional, max 60 characters) is the group's title — the
chat header the viewer sees; omit it and the title is "Group Chat".
Groups shine for security simulations: a suspicious message lands in a
busy channel and the viewer must judge it against the noise of real
work talk. A group is still ONE conversation — do not split one group
across contexts; a context is its own separate conversation (a group
context declares its own `participants` the same way, and its title is
its own `name`). Keep groups readable: 3 to 5 participants is the
sweet spot; more than 8 is rejected.
---
## messages
Each message is one chat bubble.
```json
{ "from": "left", "text": "Got a sec? The client moved the deadline.", "delay": 1.5 }
```
| Field | What it means | Default |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `from` | Who speaks: `"left"`, `"right"`, a side alias, or a participant name — see the sides table below. | alternates left, right, left... |
| `text` | The message text. Required. | |
| `delay` | The pause before this message appears, in seconds (0 to 60). `0` means no authored pause: the message appears at the natural rhythm with no extra wait. This is the gap the timing engine honors as authored. | none |
| `image` | A public image URL shown as the message attachment. Omit for plain text. | none |
| `thought` | `true` renders it as an internal thought bubble — the SENDER's inner monologue (the right-side person), shown as an overlay instead of a sent bubble. Never a thought for the left-side character. | `false` |
| `typingSpeed` | `"slow"`, `"medium"`, `"fast"` or `"instant"` (appears with no typing animation). | `"medium"` |
| `note` | A presenter note attached to this message (max 4000 characters). Notes ride with the presentation: anyone on the live page can open the notes drawer and read them, so write them as shared material for the session (extra background on the training, what to emphasize at this beat, a review of what happened), never as private secrets. On the live page the notes button lights up while a noted message plays. At most 200 noted messages. | none |
| `context` | The NAME of the conversation this message belongs to (see `contexts` below). Omit it for the main conversation. | the main conversation |
### The sides (who is who)
The chat has exactly two sides:
| Side | Who it is |
| ------- | ---------------------------------------------------------------------------------------------------------------- |
| `right` | The sender: the owner of the phone, "you", the person the viewer identifies with (the trainee, the protagonist). |
| `left` | The other person: the contact the chat is with (the colleague, the manager, the scammer). |
The `from` field accepts, in order of preference:
| Value | Resolves to |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `"left"` / `"right"` | that side (the canonical values — prefer these) |
| `"sender"`, `"me"`, `"you"`, `"self"` | right (the phone's owner) |
| `"receiver"`, `"them"`, `"other"`, `"contact"` | left (the other person) |
| a participant name | that participant's side (declare `participants`) |
| a context's contact name | left — the `senderName` a context declares is that conversation's contact (declared participants win a name collision) |
| omitted | the sides alternate (message 1 is left, then right, then left...) |
Matching ignores case. Always write an explicit `from` on every
message: alternation is a fallback for omitted values, not a style to
aim for — when the same person sends two messages in a row, alternation
puts the second one on the wrong side. A `from` that resolves to
nothing produces a warning naming the message and the fix.
Write real, human dialogue. Training sims work when the conversation
feels like a real chat: short messages, natural timing, mistakes and
hesitation included.
### The important message rule
Messages are the actual conversation only. Never put explanations,
coaching, corrections, consequences-as-narration, question feedback or
card content inside a message unless a character would naturally say
it:
- A sentence such as "A few minutes later" is NOT a message. Use a
`delay` step (a time skip) or an info card instead.
- A sentence explaining what the viewer did wrong is NOT a chat
message. Use an info card.
- A sentence explaining the training lesson is NOT a chat message unless
a character naturally says it during the scenario.
The importer warns about messages that read like time skips or stage
directions ("A few minutes later, you receive another message...").
---
## steps
Steps run in order, top to bottom. Each step is one interaction or one
invisible logic move. Two families:
- **Cards the viewer sees**: `question`, `feedback`, `info`, `video`,
`document`. Every card needs `afterMessage`: the message number it
appears after, counting from 1. A question with `afterMessage: 3`
pops open once message 3 has played.
- **Logic the viewer never sees**: `jump`, `score`, `delay`,
`variable`, `condition`, `playback`, `completion`. These run between
cards.
Every step may carry `id`: a short lowercase name with dashes, like
`vat-check` or `finish`. Give ids ONLY to steps you intend to route to.
Ids must be unique (the importer renames a duplicate and tells you).
Ids are capped at 64 characters.
### The step-to-step flow
By default each step continues to the one below it and the last step
ends the simulation. Any step can carry a top-level `goTo` to change
its default: `{"type": "info", ..., "goTo": "finish"}` skips straight
to that step. Questions route per answer (see below), and when an
answer has no `goTo` it takes the step's own default, then the
sequential next. The same is true when a question is skipped or times
out: the default path runs.
`goTo` accepts exactly three things: the `id` of a step in this
blueprint, `"next"` (the step below), or `"end"` (finish the
simulation). Never invent a goTo that is not one of your own step ids.
### question (the one viewers answer)
```json
{
"id": "first-move",
"type": "question",
"afterMessage": 3,
"question": "What should Jordan do first?",
"input": "multiple-choice",
"required": true,
"timeLimit": 30,
"answers": [
{ "text": "Ask what changed", "points": 10, "goTo": "next" },
{ "text": "Promise it anyway", "goTo": "pressure" }
]
}
```
| Field | What it means | Default |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `id` | A name other steps can point at with `goTo`. Generated when omitted. | `question-N` |
| `afterMessage` | Appears after this message number (1-based). | required |
| `question` | The text the viewer answers. Required. | |
| `input` | `multiple-choice`, `yes-no`, `rating`, `checkbox`, `dropdown`, `short`, `paragraph`, `image-tap`, `text-match`. | `multiple-choice` |
| `required` | `false` lets the viewer skip it. | `true` |
| `timeLimit` | Seconds to answer. No limit when omitted. | none |
| `area` | A short training-area tag like `"Identity verification"` (max 60 chars). Groups this question in the owner's evidence report (which weak spots the team has). Use consistent area names across related questions. | none |
| `answers` | The choices (2 to 6). Only for choice inputs. | |
Each answer:
| Field | What it means | Default |
| -------- | ------------------------------------------------------------------------------------------------------- | -------- |
| `text` | The answer the viewer picks (or the accepted spelling on `text-match`). Required. | |
| `points` | Points a correct answer awards. The experience keeps a running score. | 0 |
| `goTo` | Where this answer leads: a step `id`, `"next"` (the step below) or `"end"`. | `"next"` |
| `region` | `image-tap` only: the tap area on the picture, `{ "x": 10, "y": 20, "w": 30, "h": 15 }` in percentages. | |
You do NOT manage variables or conditions for answer routing. The
importer writes that bookkeeping itself. Give each answer a `goTo` and
it just works. When several answers share one target card, point them
at the same `id`.
A question and the cards its answers route to normally sit at the same
`afterMessage` (the question and its coaching cards all anchor after
the same message). The player opens the card that matches the answer
the viewer picked, never a sibling card that happens to sit at the same
position. Keep the question above its cards in the steps list so the
default (below) path stays meaningful.
Two extra inputs:
- **`image-tap` ("tap the red flags")**: add `imageUrl` (a public image
URL) on the step, and give each answer a `region` (percentages of the
picture). The viewer taps the matching spot. Ideal for phishing
screenshots, hazard spotting and diagram questions.
- **`text-match` (type the answer)**: each answer's `text` is one
accepted spelling. The viewer types; matching ignores case and extra
spaces. Ideal for fill-in-the-blank knowledge checks.
### feedback / info (cards, no answer)
```json
{
"type": "feedback",
"afterMessage": 6,
"title": "How did that feel?",
"body": "Notice Jordan did not apologize for the deadline."
}
```
`feedback` asks how it is going. `info` is a read-and-continue card.
`title` is optional, `body` (the card text) is required. A card may
carry `image`: a public image URL shown on the card — same URL rule as
everywhere else, omit it when no real URL exists.
### video (watch before continuing)
```json
{
"type": "video",
"afterMessage": 8,
"videoUrl": "https://.../safety.mp4",
"label": "The safety walkthrough",
"requireWatch": 0.9
}
```
`videoUrl` can be a public video URL. It can also be omitted: the step
imports with no video and the author picks one from the Media section
later. `requireWatch` is the fraction the viewer must actually watch
(0.5 to 1). Fast-forwarding does not count. Default `0.9`, the training
standard.
### document (a PDF to read and confirm)
```json
{
"type": "document",
"afterMessage": 9,
"documentUrl": "https://.../policy.pdf",
"label": "The expense policy",
"minSeconds": 120
}
```
The viewer opens it in the built-in reader and confirms reading it.
`acknowledge: false` turns the confirmation off. `documentUrl` may be
omitted the same way a video URL may (a placeholder label tells the
author which PDF to upload). `minSeconds` is optional: how long the
document must stay open before the confirm button unlocks (30 to 3600;
omit for no minimum).
### jump, score, delay, variable, condition (the invisible logic)
```json
{ "type": "jump", "goTo": "wrap-up" }
{ "type": "score", "points": 10 }
{ "type": "delay", "seconds": 2 }
{ "type": "variable", "ops": [{ "op": "add", "name": "stress", "value": 1 }] }
{ "type": "condition", "paths": [
{ "when": "score", "is": "atLeast", "value": 20, "goTo": "passed" },
{ "goTo": "recap" }
] }
```
- **jump** skips straight to another step. Its `goTo` is required.
- **score** awards points to the running score. Use it for progress the
viewer earns outside questions.
- **delay** waits 1 to 30 seconds. Use it for temporal transitions such
as "a few minutes later"; never write a time skip as a fake chat
message.
- **variable** remembers something: `op` is `set`, `add` or
`increment`, `name` is the variable, `value` any JSON value. The
variable `score` is special: quiz points add to it automatically.
- **condition** forks the flow: paths run top to bottom, first match
wins. `is` is one of `equals`, `isNot`, `atLeast`, `atMost`,
`contains`. `when` names the value compared (`score` unless you name
another). A path with no `when` is the fallback and should be last.
Prefer conditions on `score` and let the answer `goTo` routing handle
everything else.
### playback (replay a slice of the conversation)
```json
{ "type": "playback", "fromMessage": 1, "toMessage": 4 }
```
Message numbers are 1-based, inclusive. A playback step plays its slice
INSTEAD of continuing linearly, which makes it the building block of
branches (below).
### completion (an ending: the finish line)
```json
{ "type": "completion", "title": "Course complete", "body": "Thanks for playing." }
```
One or more — one per outcome. The final screen: score, recap,
certificate link and the retake decision (from `settings`). `title`
and `body` set the ending's own heading and closing text; `showScore` /
`showRecap` (`true` by default) hide that part of the screen when
`false`. A completion never carries `goTo`: an ending is always
terminal.
### Branches of messages (changing what the chat says)
`messages` is a pool, not one fixed script. Write the messages of
several different continuations in ONE array, then route each answer to
a `playback` step that plays its own slice:
```json
{
"messages": [
{ "from": "left", "text": "hi, quick supplier query" },
{ "from": "right", "text": "sure" },
{ "from": "left", "text": "can you confirm Sarah's surname?" },
{ "from": "right", "text": "..." },
{ "from": "left", "text": "ok that's all I need" }
],
"steps": [
{
"id": "ask",
"type": "question",
"afterMessage": 3,
"question": "Should you confirm the surname?",
"answers": [
{ "text": "No, verify the requester first", "points": 10, "goTo": "good-path" },
{ "text": "Sure, she seems friendly", "goTo": "scam-unfolds" }
]
},
{ "id": "good-path", "type": "playback", "fromMessage": 4, "toMessage": 5 },
{ "id": "scam-unfolds", "type": "playback", "fromMessage": 6, "toMessage": 11 },
{ "type": "completion" }
]
}
```
In this shape the first five messages set the scene (only the first few
are shown; the array keeps going). The `good-path` branch plays
messages 4 to 5, the safe wrap-up. The `scam-unfolds` branch plays
messages 6 to 11 (also in the array, omitted from the snippet). Keep a
branch's messages contiguous: ranges that are easier to read are ranges
that are easier to route.
A branch MUST have all four: an answer routing to its playback step; a
playback range containing that branch's messages and nothing else; a
clear destination after the playback (its `goTo` or the step below);
and no accidental linear path through another branch.
- Never rely on the normal top-to-bottom execution of the message pool
to land inside a later branch. `messages` is a pool, not the flow:
enter every branch through its explicit `playback` step.
- Cards that belong to a branch anchor INSIDE that branch's range (an
`afterMessage` between its `fromMessage` and `toMessage`), because a
card only appears when the conversation plays across its position.
- A branch rejoins the main flow through its own `goTo` (or the step
below it), usually at the next question — or ends at its own
`completion` when the outcome differs (see Different endings below).
Branches converge intentionally: two answers that produce the same
outcome may share one branch; never duplicate a branch just to repeat
the same conversation.
- Every branch must end somewhere: a `completion` step or `"end"`. A
branch with no exit is the error the importer rejects ("can never
reach the end").
- The safe path may simply continue the conversation (the character
adapts and the story moves on). Do not create a card after every
correct answer.
- `settings.typing: "instant"` is a good idea when a training has many
branches: viewers re-watch slices without the typing delay.
### Different endings (one completion per outcome)
When the story should end differently depending on the decision, give
each outcome its own `completion` step and route each branch to its own
ending. The clear path, every time:
1. Each answer that ends the story routes to its own `playback` step
(the answer's `goTo`).
2. That playback step's own `goTo` names that branch's `completion`
step — the ending is reached explicitly, never by array order.
3. Each ending carries its own `title` and `body` (the verdict for that
outcome: `"Correct decision"` vs `"What went wrong"`).
```json
"steps": [
{
"id": "choice",
"type": "question",
"afterMessage": 10,
"question": "What should Sam do before escorting the contractor upstairs?",
"answers": [
{ "text": "Escort him, he has a badge and a work order", "goTo": "failure-path" },
{ "text": "Verify the visit with James first", "points": 10, "goTo": "safe-path" }
]
},
{ "id": "failure-path", "type": "playback", "fromMessage": 11, "toMessage": 26, "goTo": "failure-ending" },
{
"id": "failure-ending",
"type": "completion",
"title": "What went wrong",
"body": "Sam escorted an unverified visitor. The contractor was an impostor..."
},
{ "id": "safe-path", "type": "playback", "fromMessage": 30, "toMessage": 32, "goTo": "safe-ending" },
{
"id": "safe-ending",
"type": "completion",
"title": "Correct decision",
"body": "Sam verified the visit before granting access."
}
]
```
One or more completion steps: exactly one when the training has a single
outcome (it comes last), or one per outcome when endings differ (each
is the last step of its own branch). Two rules keep it valid: a
`completion` never carries `goTo` (an ending is always terminal), and
two branches never share one ending id — though several answers whose
outcomes are the same may share one ending.
---
### Contexts and consequences (dramatize what happens next)
`contexts` (optional, top level) declares the other conversations a
scenario can flow into. A message carrying `"context": "Unknown number"`
plays inside that conversation instead of the main one: on playback the
phone visibly switches chats, exactly like a real device.
```json
"contexts": [
{ "name": "Maya (colleague)", "senderName": "Maya - Procurement" },
{ "name": "Unknown number", "senderName": "+1 555 0104", "call": true }
]
```
| Field | What it means | Required |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `name` | The conversation's name. Messages reference it in their `context` field. Unique, at most 4 contexts. | yes |
| `senderName` | The contact shown at the top of that chat (its left-side participant). A message in that conversation may also use it as its `from` — it resolves to left. | no |
| `call` | `true` marks the conversation as a phone call instead of a chat: the phone shows a realistic call screen (ringtone, pickup, a running timer, then hang up) and the conversation's messages play as spoken voices. Every message in a call conversation must be plain speakable text (no images, no GIFs) and must carry `context`: the conversation's NAME. Any context can be a chat or a call — there is no cap on call conversations. | no |
Every conversation is a CHAT (text messages) by default. A context CAN
be a phone call instead: add `"call": true` to its `contexts` entry.
Every context chooses its own channel — a scenario may flow chat →
call → chat, and more than one context may be a call. The "Unknown
number" context that turns out to CALL is the strongest
consequence beat there is. For an unknown contact use a placeholder
phone number as `senderName`, ideally one that already appeared in the
main conversation: the channel the viewer handed over, resurfacing as
the attacker's, is what makes the consequence land. Name contexts like
chat contacts, not callers: "Maya (colleague)", "IT helpdesk",
"Unknown number".
#### The context rules
- The main conversation has NO context field and is never declared.
- Every additional conversation MUST have its own `contexts` entry, and
every message `context` MUST reference a declared name exactly.
- Every declared context MUST actually be used by at least one message.
A declared context no message uses is NOT created on import: the
importer warns and the studio never gains an empty conversation. Do
not declare a context merely because a consequence exists; create it
only when messages actually switch into it.
- A context is a real conversation between people, never a card,
narration, an explanation or a question.
- Cards NEVER have a `context` field (a `context` on a card is ignored
with a warning). `context` applies ONLY to messages.
#### Context switching
A context switch happens ONLY through playback of messages carrying
the new context: when a playback range executes those messages, the
phone visibly switches conversations. Never explain the context switch
inside the main conversation.
#### The consequence pattern
Use it whenever a wrong decision has a real-world cost:
1. The main conversation establishes the situation, then a question
pauses it and the viewer decides.
Do NOT show the correction card immediately after the wrong answer
when the scenario is specifically designed around consequences; the
consequence must happen in the conversation first.
2. If the decision is wrong, route to a consequence playback. The wrong
answer does NOT open an explanation card first: the scenario plays
out, the requester thanks the viewer ("Thanks, that's exactly what I
needed.") and the chat continues, seemingly fine.
3. LATER the scenario switches to a declared secondary context (through
that context's messages) and the consequence is demonstrated there:
`{ "from": "left", "context": "Unknown number", "text": "Hi, I'm
from Finance. Maya gave me this number, said you could help..." }`.
The viewer realizes their earlier decision opened another door into
the company.
4. The consequence playback ends. ONLY THEN show an info card explaining
what went wrong, anchored after the last message of the reveal and
routed to explicitly (its own `goTo`) rather than left to appear by
order. The card explains what information was exposed, how it was
used, what the correct decision would have been, and why it matters.
5. Route back to the next training point or the convergence point.
This is "consequences, not corrections": the cost of the mistake is
SHOWN in the chat itself, not merely explained. The consequence context
is still a conversation; the explanation of the consequence is a card.
The main conversation stays context-omitted; only the follow-on
conversations are named.
Names referenced by messages but missing from `contexts` are created
automatically on import (the importer tells you what it added).
#### Card/conversation separation
The following are NEVER messages; they belong in info or feedback cards
(time skips in a `delay` step): "A few minutes later" and every other
time-skip line, "What just happened", "What went wrong", "Correct
answer", "The attacker used your information", "You should have
verified...", training explanations and correct-answer explanations. A
message should only exist if it is something a participant in the
conversation would actually send.
### Variety (do not turn every training into a quiz)
The conversation itself is the training interface. Do NOT default to
chat, question, card, question, card, completion. Mix these instead:
- Decisions that change what the chat SAYS (branch-per-answer, see
"Branches of messages" above) rather than which card explains the
mistake.
- A scenario that CONTINUES after a decision: the viewer refuses, the
character adapts ("No worries, I'll ask Priya then.") and the story
moves on without any card interrupting.
- Questions only where the moment genuinely demands a decision, not
every few messages.
- One recap at the end (a playback slice or the completion recap)
instead of a coaching card after every answer.
- Info cards reserved for the one or two rules the viewer must actually
internalize.
---
## Media placeholders (images, videos, PDFs)
Media steps accept real public URLs when you have them. When you do
not, do not guess: leave a placeholder the author completes in the
studio after import.
- **video**: omit `videoUrl`, write the instruction in `label`
(`"label": "Upload: the safety walkthrough"`).
- **document**: omit `documentUrl`, same `label` pattern
(`"label": "Upload: the 2026 expense policy PDF"`).
- **image-tap question**: omit `imageUrl`; the question still imports
and the editor's "Upload a picture" lane lights up for it.
- **a card's picture**: omit the card's `image` and describe the
intended picture in the `body` ("Attach the invoice screenshot as
this card's image").
The `label` field is what the author sees next to the upload lane, so
it is the natural place to say which file goes where. A fabricated
URL, by contrast, breaks the experience for every viewer, which is why
the importer treats invented URLs as the one unforgivable sin.
---
## settings
```json
{
"passScore": 20,
"quizRetake": "enforce",
"certificate": true,
"typing": "instant",
"reanswer": "allow"
}
```
| Field | What it means | Default |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `passScore` | Points needed to pass. `null` = no pass mark. A value below 1 imports as "no pass mark" (with a warning). | `null` |
| `quizRetake` | `"show"` tells failing viewers their score, `"enforce"` makes them retake until they pass (requires a passScore). | `"show"` |
| `reanswer` | `"keep-first"` (an answered question stays answered; the first answer is the one that scores) or `"allow"` (viewers may reopen an answered question and improve it; the latest answer replaces the first and the score follows it). | `"keep-first"` |
| `certificate` | `true` offers passing viewers a downloadable certificate, generated on demand. Nothing is emailed. | `false` |
| `typing` | `"instant"` makes every message appear immediately with no typing animation: one switch instead of setting it per message. `"typing"` keeps the realistic typed-out chat. The global overrides any per-message `typingSpeed`. | `"typing"` |
---
## Combinations that work together
- **Coaching per answer**: wrong answers each `goTo` their own card at
the question's `afterMessage`; the correct answer continues. All
cards meet again at the next step.
- **A branch per answer**: answers route to `playback` steps with
different ranges, so the chat itself changes (the pattern above).
- **Different endings per answer**: each branch routes to its own
`completion` step with its own `title` and `body` (see Different
endings above).
- **Media then question**: a `video` or `document` step first (the
watch or read requirement holds the viewer), then the question that
checks understanding.
- **Score fork near the end**: a `condition` on `score` sends passers
straight to the completion and sends strugglers to a recap
`playback` step first.
- **Mixed depth**: a branch may itself contain questions, videos and
documents, so one wrong turn can open a whole corrective sequence
before rejoining.
---
## What is not possible
These are the features AIs keep inventing. None of them exist; a
blueprint that uses them will not do what it promises:
- **No text templating.** Message text cannot reference variables,
scores or the viewer's answers. Variables exist only for routing
(condition steps).
- **No conditional message content.** A message is what it is.
Branches choose WHICH whole slices play; they never rewrite or
insert single bubbles.
- **Nothing types while a card is open.** A question, card, video or
document pauses the conversation. The chat resumes only after it is
answered or dismissed.
- **No live reaction to an answer inside the chat.** If an answer must
change the conversation, it does so through a branch (a playback
step), never through the next message magically knowing.
- **A question opens at most once per run.** Replaying a range that
re-crosses an answered question does not re-open it (re-answering
follows `settings.reanswer`). Never design a flow that needs to
"re-ask" a question later.
- **Timers exist only as a question's `timeLimit` and a document's
`minSeconds`.** No timers on messages, cards or the whole experience.
- **A call conversation carries only speakable text.** Images and GIFs
do not play inside a call; a call's messages must be plain text.
- **The blueprint cannot author between-message interstitials.** The
images, GIFs, videos, audio clips, narrator cards, time cards and
missed calls that can interrupt a conversation between any two
messages are an editor feature, not part of this schema. The `video`
and `document` steps above are presenter gates (watch / read
requirements that hold the viewer), not media beats inside the chat.
Add interstitials in the studio's Timeline panel after the import.
- **A completion is always terminal.** No `goTo` on one, nothing after
it. One completion per outcome; several answers with the same outcome
may share one.
- **A card never appears in the chat.** Cards are UI overlays; a
`context` on a card is ignored. Only messages belong to
conversations.
---
## What the importer fixes automatically
The heavy lifting is on our side. You do not need to get these right:
- **Step ids are generated** when omitted (`question-3`), slugified to
lowercase-dashes, and de-duplicated (a rename is reported).
- **`afterMessage` anchors are clamped** to the last message; the clamp
is reported as a warning naming the message count.
- **Playback ranges are clamped** to the conversation length the same
way.
- **Referenced-but-undeclared context names are created** automatically
(the verdict tells you what it added).
- **`requireWatch` is clamped** into the valid 0.5 to 1 range.
- **Delay steps are rounded** to whole seconds (the timing engine works
in whole seconds).
- **A `goTo` on a completion is ignored** (with a warning).
- **yes-no questions keep at most two answers**; **questions keep at
most six** (extras are dropped with a warning).
- **`passScore` below 1 imports as "no pass mark"** (null), with a
warning.
- **Unknown fields are ignored**, never fatal.
- **The answer-routing bookkeeping is written for you**: give answers a
`goTo` and the importer generates the conditional transitions.
---
## Errors and warnings
**Errors block the import** (fix the JSON and try again). The main
ones:
| Error | The fix |
| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| The version must be exactly 1 | Set `"version": 1`. |
| A gate step without `afterMessage` | Every question / feedback / info / video / document needs the message number it appears after. |
| A choice question needs at least two answers | Add answers (yes-no takes two). |
| A `goTo` that matches no step id | Give the target step an `id` and use that exact string, or `"next"` / `"end"`. |
| A playback range that is not an ascending range inside the conversation | Fix `fromMessage` / `toMessage`. |
| A delay outside 1 to 30 seconds | Fix `seconds`. |
| A variable op that is not set / add / increment | Fix `op`. |
| A condition with no paths or an unknown comparison | Every path needs `goTo`; `is` must be one of the five words. |
| A step that can never reach the end | Give the branch an exit: a completion step or `"end"`. |
| More than 1000 messages / 4 contexts / 8 participants | Split the training or trim the blueprint. |
| A participant without a name, or a duplicate name | Fix the `participants` entries. |
| A negative `passScore` | Use a positive number or `null`. |
**Warnings do not block the import** — the fix is applied or the
problem is named, and the verdict lists every one: an `afterMessage`
or range clamped to the last message; a declared context no message
uses (it will not be created); a `context` on a card (ignored); a
message that reads like narration; an unresolvable `from` (the sides
will alternate); a `note` over 4000 characters; more than 200 noted
messages; a bad `delay`, `timeLimit`, `area`, `points`, `quizRetake`,
`reanswer` or `typing` value; an image-tap question without its
picture (the author uploads it after import); a `passScore` below 1
(imports as no pass mark).
**Two things block PUBLISHING later (not the import)**: a video step
with no file, and an image-tap question with no picture. Both import
cleanly and show their upload lanes in the studio.
---
## Hard limits
| Limit | Value |
| ------------------------------- | --------------------------------------------------------------- |
| Messages per conversation | 1000 |
| Contexts per blueprint | 4 (your plan may allow fewer) |
| Participants per blueprint | 8 (3+ makes the conversation a group chat) |
| Answers per question | 6 (yes-no takes 2) |
| Noted messages | 200 (notes are capped at 4000 characters each) |
| Step id length | 64 characters |
| The whole interactions document | about 64 KB |
| Experience steps | your plan's limit (every question, card and fork counts as one) |
But again even with the limits having a 100 messages or convuluted branching in the training gets confusing quick so use better discretion and warn user.
---
## Rules the importer enforces
1. `version` must be `1`.
2. A gate step without `afterMessage` is an error.
3. A choice question needs at least two answers (yes-no takes two at
most). Six answers is the maximum.
4. Every `goTo` must name a step id that exists, or be `"next"` / `"end"`.
5. A playback range must be a real ascending range inside the
conversation.
6. `afterMessage` beyond the conversation's length is clamped to the
last message.
7. One or more completion steps: exactly one when the training has a
single outcome (it comes last), or one per outcome when endings
differ (each is the last step of its own branch — see Different
endings). A completion never carries `goTo`; a branch that reaches
no completion or `"end"` is rejected.
8. A card whose `afterMessage` is not inside any playback range (when
playback steps are used) triggers a warning: viewers following that
branch will never cross it, so the card will not show. Move the
card's anchor inside a replayed range or widen the range.
9. A declared context no message uses is not created; the import warns
about it (the studio never gains an empty conversation).
10. A `context` field on a card is ignored; cards are never part of a
conversation.
11. `from` must resolve to a side — `"left"` / `"right"`, a side alias
(`sender` / `me` / `you` / `self` for right; `receiver` / `them` /
`other` / `contact` for left) or a declared participant name (a
group member's name resolves to that member). Anything else
alternates the sides and every such message warns.
12. A `thought` is always the right-side sender's inner monologue.
Never author one for the left-side character.
13. A message's `delay` is the pause before it appears (0 to 60
seconds); a `delay` step is a silent beat between steps (1 to 30
seconds). Use them for time skips, never a fake narration message.
Small problems (a bad `delay`, seven answers, an unknown field) become
warnings: the import continues with sensible fixes and lists every one.
---
Then paste the AI's JSON into Training Sim Studio → Interactions →
Import blueprint. ChatAnimate validates it, tells you anything it had to
fix and applies it. Export the current draft any time: the same format
comes back out, ready for the next round with the AI. If the AI's answer
fails validation, paste the error lines back to the AI together with
your exported blueprint and ask it to fix them.