Skip to main content

What is a Policy?

The workflow does whatever you tell it to. Policies decide whether it should run at all. They run first, before any action fires. If any policy fails, the whole run is blocked and you get a receipt explaining why. A policy is a plain Python function — no LLMs, no magic. It reads the context and returns pass or fail with a reason.

Writing a Policy

Here’s a concrete example. The standard engineering rule: no one pushes directly to main. Agents break this constantly because no one told them not to. The Amazon Kiro incident was exactly this pattern — a direct infrastructure change with no review step caused a 13-hour AWS outage.
from enact.models import WorkflowContext, PolicyResult

def dont_push_to_main(context: WorkflowContext) -> PolicyResult:
    branch = context.payload.get("branch", "")
    branch_is_not_main = branch.lower() not in ("main", "master")
    return PolicyResult(
        policy="dont_push_to_main",
        passed=branch_is_not_main,
        reason="Branch is not main/master" if branch_is_not_main else f"Direct push to '{branch}' is blocked",
    )
How the check works:
┌─────────────────────────────────────────────────────────┐
│  STEP 1: Read the branch name from the agent's request  │
│    context.payload.get("branch", "")  -->  "main"       │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  STEP 2: Is this branch safe to push to?                │
│    branch_is_not_main = branch not in ("main","master") │
│                                                         │
│    "agent/fix-149" -->  branch_is_not_main = True   ✅  │
│    "main"          -->  branch_is_not_main = False  🚫  │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  STEP 3: passed = branch_is_not_main                    │
│                                                         │
│    True   -->  PASS ✅                                  │
│    False  -->  BLOCK 🚫                                 │
└─────────────────────────────────────────────────────────┘

Registering Policies

Pass them to EnactClient. Multiple policies = all must pass:
from enact import EnactClient
from enact.policies.git import dont_push_to_main, require_branch_prefix

enact = EnactClient(
    systems={...},
    policies=[
        dont_push_to_main,                # plain policy function
        require_branch_prefix("agent/"),  # policy factory — returns a configured function
    ],
    workflows=[...],
    secret="your-secret",
)

Built-in Policies

Enact ships 30 built-in policies across 9 categories:
CategoryPoliciesWhat they block
Gitdont_push_to_main, require_branch_prefix, max_files_per_commit, dont_delete_branch, dont_merge_to_mainDirect pushes to main, wrong branch names, blast radius
Databasedont_delete_row, dont_delete_without_where, dont_update_without_where, protect_tables, block_ddlDangerous deletes, unscoped updates, DDL like DROP TABLE
Filesystemdont_delete_file, restrict_paths, block_extensionsFile deletions, path traversal, sensitive files (.env, .key)
Accesscontractor_cannot_write_pii, require_actor_role, require_user_role, dont_read_sensitive_tables, dont_read_sensitive_paths, require_clearance_for_pathUnauthorized access, PII exposure
CRMdont_duplicate_contacts, limit_tasks_per_contactDuplicate records, rate limiting
Timewithin_maintenance_window, code_freeze_activeActions outside allowed hours, during code freezes
Slackrequire_channel_allowlist, block_dmsOff-list channel posts, direct messages to users
Emailno_mass_emails, no_repeat_emailsMass email blasts, spamming the same recipient
Cloud Storagedont_delete_without_human_okS3/GDrive deletions without cryptographic HITL approval

Import Paths

from enact.policies.git import dont_push_to_main, require_branch_prefix
from enact.policies.db import protect_tables, block_ddl
from enact.policies.time import code_freeze_active
from enact.policies.slack import require_channel_allowlist, block_dms
from enact.policies.email import no_mass_emails
from enact.policies.cloud_storage import dont_delete_without_human_ok

Naming Convention

Name booleans after what you want to be true — not what you’re guarding against:
# Bad — forces a mental flip to understand
blocked = branch.lower() in ("main", "master")
passed = not blocked

# Good — reads like a sentence
branch_is_not_main = branch.lower() not in ("main", "master")
passed = branch_is_not_main