Authoring
Scenario authoring¶
Scenarios are built in the visual scenario builder (Scenarios → New scenario) — no JSON editing required. JSON is the interchange format: import a pre-built scenario file with Import JSON, and export any scenario from its detail page to share it or version it.
The builder¶
The builder is a three-pane workspace:
- Outline (left) — switch between the Scenario brief (title, description, author, estimated duration, tags, start inject, debrief notes), Participant teams (the routing groups injects can target), and the list of injects, each badged as start, branch, linear, or end.
- Editor (centre) — the selected inject: its ID, title, and content; whether participants may submit a free-text response; target teams (chips — leave all unchecked for a shared inject); progression (branch options, or a linear next inject when there are no options); and expected actions — evaluator cues shown in the facilitator console.
- Readiness (right) — inject/branch/targeting counts, a live validation list (blocking issues disable saving), and a flow preview of the path from the start inject plus any disconnected nodes.
JSON format¶
Top-level structure¶
{
"schema_version": "1.0",
"title": "Ransomware incident",
"description": "A simulated ransomware attack affecting core infrastructure.",
"tags": ["cyber", "ransomware"],
"metadata": { "author": "IcebergTTX", "estimated_duration_minutes": 90 },
"participant_teams": [
{ "id": "it_ops", "label": "IT Operations" },
{ "id": "legal", "label": "Legal & Compliance" }
],
"start_inject_id": "inject_01",
"injects": [ ... ],
"debrief_notes": "Key learning: notify ICO within 72 hours."
}
An inject¶
{
"id": "inject_01",
"title": "Ransomware detected",
"content": "SOC has detected encrypted files on 3 servers...",
"target_teams": ["it_ops"],
"free_text_response": true,
"sequence_order": 1,
"release_at_minutes": 15,
"next_inject_id": null,
"options": [
{ "id": "opt_isolate", "label": "Isolate affected systems immediately", "next_inject_id": "inject_02a" },
{ "id": "opt_monitor", "label": "Monitor and gather more information", "next_inject_id": "inject_02b" }
],
"expected_actions": ["Notify CISO immediately", "Preserve forensic evidence"],
"triggers_communications": [
{
"direction": "inbound",
"external_entity": "NCSC",
"subject": "Ransomware advisory",
"body": "We have been made aware of a campaign targeting...",
"delay_after_release_seconds": 120
}
]
}
Fields¶
target_teams- IDs from
participant_teams. A blank array (or omitted) means all teams receive the inject. options- Branch choices shown to participants. Each points at the next inject via
next_inject_id;nullends the branch. next_inject_id(node-level)- Linear continuation for injects without branch choices — the participant submits a free-text response, then the facilitator releases the next inject. This chains injects into a straight-line sequence.
release_at_minutes- Optional. Auto-releases this inject that many minutes after the exercise
starts, instead of waiting for the facilitator. The console shows a live
countdown, and the clock is pause-aware — pausing defers the timer and
resuming re-arms it with the remaining offset. The facilitator can still
release it early or cancel the schedule. Omit (or
null) for manual release only, which is the default. expected_actions- Evaluator cues shown alongside responses in the facilitator console (and used by the LLM assessment when enabled).
triggers_communications- Messages automatically injected into the comms inbox when this inject is
released. Triggered inbound messages are visible to all teams. Triggered
outbound messages are facilitator-visible because they have no participant
sender or recipient-team scope.
delay_after_release_secondsstaggers delivery.
Validation
The builder's readiness pane validates as you type, and blocking issues
disable saving; the scenario detail page shows the same validation sidebar.
Every next_inject_id reference must exist, and node-level and per-option
next_inject_id edges are checked for cycles — linear chains can't loop.
Branching model — the participants choose the path, the facilitator controls the pace¶
The two decisions belong to different people, and it is worth being precise about which:
| Who decides | ||
|---|---|---|
| Which inject comes next | The participants | The selected option's next_inject_id advances that team's cursor to exactly one node |
| Whether and when it is released | The facilitator | Release it now, later, or never — but the branch itself is already settled |
When a team responds, the option they picked resolves to a single next node and moves that
team's progression cursor to it. The facilitator then reviews the response and releases
that inject when the room is ready. What the facilitator cannot do is overrule the
choice: releasing the branch the team did not pick is rejected with
409 Inject is not the current branch for its group.
So the scenario never picks a branch on its own: the route through the tree is the participants' to choose. That is the point — their decisions have to actually carry consequences.
Delivery, on the other hand, can be automatic. An inject carrying
release_at_minutes releases itself on a countdown, with no facilitator action.
A timer decides only when an inject lands: it never selects a branch. On a node the team
has not reached yet, the release is deferred until their response unlocks it — arriving
immediately, if the offset has passed by then. On an unreferenced node — one no branch
links to — it simply fires on its clock, which is how you run a parallel timeline.
One response settles the branch for the whole team
The cursor is per team, not per person. The first response resolves the inject for the team and commits it to that branch; the alternatives can no longer be released to them.
AI assessment¶
When enabled on an exercise (and an AI provider is configured via LLM_PROVIDER —
Anthropic, Amazon Bedrock, OpenAI, Ollama, or Gemini), the model evaluates each
participant response and produces:
- a decision-quality rating — good, adequate, or poor;
- a brief assessment of the reasoning;
- a suggested follow-up inject the facilitator can approve and queue.
Assessments appear in the AI-assessment column of each response card, and in the right pane's AI-suggestions widget. The Flagged filter surfaces responses the AI rated as poor.
Running an exercise¶
- Create or import a scenario — build it in the scenario builder, or load a JSON file.
- Create an exercise — give it a title, select a scenario, optionally enable AI assessment.
- Add participants — search registered users in the Participants panel and
enrol them; each is assigned a team. Share
/exercises/{id}/participate. - Start and release injects — press Release to push an inject; participants
receive it instantly over WebSocket. An inject carrying
release_at_minutesalso auto-releases on a pause-aware countdown, and you can pre-empt or cancel that. Review responses and team comments, then release the branch the team's choice resolved to. Pause halts new submissions (and defers any pending timers). - Inject communications — from Communications, click Inject inbound to simulate a message from an external entity (ICO, NCSC, CEO…) targeted at specific teams.
- Complete and export — close the exercise, then export the full transcript (JSON — injects, responses, comments, members) or the responses table (CSV).
Scenario packs
Scenarios can be exported from the detail page and re-imported into a different
IcebergTTX instance — useful for sharing scenario packs between teams. Two
sample scenarios (ransomware_response, vendor_outage) ship bundled and can be
loaded from Settings.