Author a playbook your on-call trusts.
A playbook is a directed graph the response engine runs when a detection fires: a trigger, a few action nodes, decision branches, and the approval gates that keep autonomy honest. This guide takes one from an idea to a deployed, observable runbook — and every action it takes is reversible.
The playbook model
A playbook is a graph, not a script. Nodes are typed; edges are transitions. The engine walks the graph per case, recording every step as a signed event on the fabric, so the run is auditable and — because each action carries its inverse — undoable.
| Node | Does |
|---|---|
trigger | Binds the playbook to one or more detections or case events. |
action | A reversible operation — enrich, isolate, disable, revoke, block, notify. |
branch | Routes on enriched context (severity, asset tier, intel match). |
gate | Pauses for approval, with a timeout and a fallback path. |
terminal | Closes the run — resolved, escalated, or handed to a war room. |
Author the graph
Start from the trigger and follow the worst-case path. The example below enriches the entity, branches on asset criticality, contains immediately for crown-jewel assets and gates everything else, then pulls the right people into a war room.
YAMLplaybooks/contain-compromised-identity.yml version: 1 id: contain-compromised-identity on: detection: [svc-account-from-corp-ip, kerberoasting-burst] steps: - id: enrich action: enrich.entity with: [asset.tier, identity.privilege, intel.match] - id: decide branch: - when: asset.tier == "crown-jewel" goto: contain-now - else: contain-gated - id: contain-now action: identity.disable # reversible by default goto: notify - id: contain-gated action: identity.disable gate: requires_approval: true timeout: 10m on_timeout: notify-only # never auto-act past the timeout goto: notify - id: notify action: notify.warroom channel: sev1 terminal: escalated
Gates, timeouts & safe mode
A gate is where you decide how much autonomy the moment deserves. Set requires_approval for actions whose blast radius you are not ready to hand to a machine; give every gate a timeout and a fallback so a sleeping approver never strands a case.
Dry-run against a recorded case
Never deploy a playbook you have not watched run. td playbook simulate replays a real, recorded case through the graph with every action stubbed — you see the path it would take and the gates it would hit, with nothing actually executed.
SHELLsimulate $ td playbook simulate contain-compromised-identity --case case_7Qd2 -> loading recorded case case_7Qd2 (4 entities, 11 events) -> enrich.entity ........ asset.tier=crown-jewel -> branch decide ........ -> contain-now -> identity.disable ..... STUBBED (simulate) -> notify.warroom ....... STUBBED (simulate) ok path resolved in 3 steps . 0 gates hit . est. MTTC 9s
Deploy & observe
Deploying binds the playbook to its detections and starts the engine routing live cases through it. Treat the run history like any production service: watch median time-to-contain, the share of cases that resolved without a human, and how long approvals actually take.
SHELLdeploy $ td playbook deploy ./playbooks/contain-compromised-identity.yml --env prod -> validating graph ...... 6 nodes, 0 unreachable -> binding 2 detections ... ok ok playbook pb_5Hk live . routing new cases now
| Signal | Last 30 days |
|---|---|
| Cases routed | 142 |
| Resolved without a human | 38% |
| Median time-to-contain | 41s |
| Approval latency (p50) | 3m 12s |
| Actions reversed | 2 |
Where to go next
- Response engine — the reversible action model these nodes compile to.
- Detection-as-code — author the triggers a playbook binds to.
- War room — where an escalated run lands.