Playbook
An event-driven automation rule in Pact: how PlaybookEngine matches a trigger event, evaluates conditions, and fires audited actions with per-account idempotency.
Playbook
A playbook is an event-driven automation rule: when a specific event
fires on an account's timeline, if a set of conditions all pass, then run a
list of actions. The engine is PlaybookEngine in core/playbook.py, and
playbooks are managed under /v1/playbooks (api/routes/playbooks.py), gated
by require_module("marketing").
Trigger and evaluation
Each playbook watches a single trigger_event. PlaybookEngine.evaluate(...)
is called with an incoming event and the account_row; it iterates the
loaded playbooks and only considers ones whose trigger_event matches the
event's event_type (and whose tenant matches). For a matched playbook,
all conditions must pass for the actions to fire.
Conditions
A condition is field op value, where field is a dot-path into the
evaluation context. The context is the account row merged with the triggering
event under _event — so icp_score, revenue_usd, _event.event_type, and
_event.metadata.job_id are all addressable. Supported operators (_OPS in
core/playbook.py):
eq, ne, gt, gte, lt, lte, contains, not_contains, exists,
not_exists, in, not_in.
An unknown operator or a raised comparison evaluates to False (fail-closed),
never an error.
Actions
Four action types are supported:
enqueue_job— enqueue a background job.set_stage— updatecompanies.crm_stage; when the stage actually changes, it also records a stage transition viacore.activity.record_stage_transition(changed_by="playbook").enroll_sequence— enqueue enrollment of the account into a sequence.notify_webhook— enqueue an outbound webhook (fire_webhook).
Unknown action types are silently ignored, so a newer playbook definition degrades gracefully on an older engine.
Idempotency and audit
Every firing is written to playbook_run_log. When a playbook sets
run_once_per_account=True, the engine checks playbook_run_log before firing
and skips accounts it has already acted on — so a re-delivered event can't
double-fire.
Dry-run testing
POST /v1/playbooks/{id}/test calls evaluate(..., dry_run=True). In dry-run
mode no DB writes or job enqueues occur — it returns the list of actions
that would fire (each record marked status="dry_run"), so you can validate a
playbook against a simulated event before enabling it.
curl -X POST https://api.pact.place/v1/playbooks/PB_ID/test \
-H "Authorization: Bearer $PACT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event": {"event_type": "deal_stage_changed", "metadata": {}}, "account": {"icp_score": 82}}'
Playbook vs journey
A playbook is a stateless rule that reacts to one event. A
journey is a stateful, multi-step
graph a subject travels through over time. Playbooks are a good way to
start a journey (via enroll_sequence or a job), not a
replacement for one.