Playbook Authoring & Deployment¶
pyfsr can author FortiSOAR playbooks from YAML and deploy them through the same import path the UI uses. You write a collection as readable YAML, an optional compiler turns it into the FortiSOAR export envelope, and pyfsr pushes it to the appliance — no hand-building of workflow/step/route JSON.
See also
Runnable examples:
examples/deploy_playbook_from_yaml.py
(YAML → compile → deploy),
examples/create_safe_playbook.py
(hand-built JSON), and the sample
examples/playbooks/yaml_demo.yaml.
The compiler is an optional extra¶
The YAML→JSON compiler ships separately from core pyfsr. Install it with the
playbooks extra:
pip install "pyfsr[playbooks]"
Core pyfsr never imports it. Until it’s installed, the compile/deploy entry
points raise PlaybooksExtraNotInstalled with that exact
hint. The non-authoring collection methods
(import_from_file(),
list, delete, …) work without it.
Writing a playbook in YAML¶
A playbook file describes one collection and its workflows. The smallest useful shape (see the sample for the full file):
collection: pyfsr YAML Demo
description: Authored in YAML, deployed with pyfsr.
visible: true
playbooks:
- name: pyfsr YAML Demo - Stamp Result
is_active: false
steps:
- name: Start
type: start
next: Set Result
- name: Set Result
type: set_variable
vars:
greeting: hello from pyfsr
source: yaml
The YAML schema (step types, their arguments, routing) is owned by the
fsr_playbooks compiler. The compiler validates every step against a reference
catalog of FortiSOAR step types and emits diagnostics (with code, path,
message, and often a suggestion) when something won’t import.
Tip
For the full DSL — every top-level key, every step type, the friendly
fields each accepts, and the start_on_create / start_on_update record
triggers — see the Playbook YAML Syntax Reference.
Deploying from Python¶
The high-level path lives on client.workflow_collections. Compile and import
in one call:
from pyfsr import FortiSOAR
client = FortiSOAR(base_url="https://fortisoar.example.com", auth="<api-key>")
created = client.workflow_collections.import_from_yaml(
"alert_triage.yaml",
replace=True, # hard-delete + recreate a same-uuid collection
)
for col in created:
print(col["name"], col["uuid"])
To inspect diagnostics before pushing anything, compile first (offline, no network) and check the result:
result = client.workflow_collections.compile_yaml("alert_triage.yaml")
if not result.ok:
from pyfsr.authoring import format_diagnostic
for diag in result.blocking:
print(format_diagnostic(diag))
else:
print("collections:", result.collection_names)
print("playbooks:", result.playbook_names)
client.workflow_collections.import_export(result.fsr_json, replace=True)
The CompiledPlaybook shape is doctested (compilation is offline, no network).
ok is True only when there are no blocking errors; collection_names and
playbook_names read off the produced envelope:
>>> from pyfsr.authoring import compile_playbook_yaml
>>> yaml = '''
... name: demo-triage
... description: Doctested example playbook
... playbooks:
... - name: Triage Alert
... description: one step
... steps:
... - name: Start
... type: start
... next: Set Note
... - name: Set Note
... type: set_variable
... manual_input:
... - name: note
... type: text
... value: hello
... '''
>>> result = compile_playbook_yaml(yaml)
>>> result.ok, result.collection_names, result.playbook_names
(True, ['00 - FSR Studio'], ['Triage Alert'])
A blocking error keeps ok False and leaves fsr_json None — errors
holds every diagnostic so you can surface why before anything is deployed:
>>> bad = compile_playbook_yaml("name: x\nplaybooks:\n - name: P\n steps: []")
>>> bad.ok, bad.fsr_json
(False, None)
>>> [e["code"] for e in bad.errors]
['no_trigger']
import_from_yaml()
options:
Option |
Effect |
|---|---|
|
Hard-delete any existing collection whose uuid matches, then recreate (the UI’s “Replace existing playbook collection” flow). Without it a duplicate uuid raises |
|
Treat compiler warnings as blocking, not just errors. |
|
Override the reference catalog (defaults to the packaged one). |
Compilation that produces blocking errors raises ValueError with the formatted
diagnostics; a missing compiler raises PlaybooksExtraNotInstalled.
The compile result¶
compile_yaml()
returns a CompiledPlaybook:
Attribute |
Meaning |
|---|---|
|
True only when there are no blocking errors and an envelope was produced. |
|
The |
|
Every diagnostic (errors and warnings) as dicts. |
|
The error-only and warning-only subsets. |
|
Convenience name lists from the envelope. |
Deploying from the CLI¶
The pyfsr playbook command group offers the same flow without writing Python.
Unlike pyfsr appliance (which uses SSH), these talk to the FortiSOAR API
and read connection details from the FSR_* environment (see
EnvConfig), with optional flag overrides.
# Compile only — emit the envelope JSON, diagnostics to stderr (no network)
pyfsr playbook compile alert_triage.yaml -o envelope.json
# Validate — compile and report a diagnostics summary; nonzero exit on errors
pyfsr playbook validate alert_triage.yaml
# Deploy — compile then import via the API client
pyfsr playbook deploy alert_triage.yaml --replace
# See what deploy would create without posting anything
pyfsr playbook deploy alert_triage.yaml --dry-run
Connection overrides (any omitted value falls back to FSR_* env):
pyfsr playbook deploy alert_triage.yaml --replace \
--server fortisoar.example.com --username csadmin --password '...' \
--port 13002 --no-verify-ssl
Importing an existing export¶
If you already have a *.json export from the UI’s Export button (no
compiler needed), import it directly:
client.workflow_collections.import_from_file("exported_playbooks.json", replace=True)
Keeping the compile catalog fresh¶
The compiler validates against a cached reference catalog of FortiSOAR step
types. pyfsr playbook check-fresh compares that catalog’s provenance against a
live appliance and flags drift (exit 0 fresh, 2 drift, 1 error/unstamped):
pyfsr playbook check-fresh --server fortisoar.example.com
If it reports drift, re-run the compiler’s warmup against the target to
refresh the catalog before deploying.