> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Operations

> Policy versioning, testing, troubleshooting, and performance considerations

## Policy Versioning

Every time you update a policy's code, Formal automatically creates a new version. This gives you a full audit trail and the ability to roll back to any previous version.

### How Versioning Works

* Each policy starts at **version 1** on creation.
* Every code update increments the version number.
* All versions are stored permanently for compliance and auditing.
* Version history includes the code and timestamp of each change.

### Viewing Version History

<Tabs>
  <Tab title="Web Console">
    1. Navigate to [Policies](https://app.formal.ai/policies)
    2. Select the policy
    3. Open the **Version History** tab
    4. Browse previous versions with their timestamps and code
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    # List all versions of a policy
    curl -X POST "https://api.joinformal.com/core.v1.PoliciesService/ListPolicyVersions" \
      -H "X-API-KEY: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"policy_id": "policy_abc123"}'

    # Get a specific version
    curl -X POST "https://api.joinformal.com/core.v1.PoliciesService/GetPolicyVersion" \
      -H "X-API-KEY: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"id": "policy_xyz789"}'
    ```
  </Tab>
</Tabs>

<Tip>
  We recommend storing your policy code in version control (e.g., Git) alongside your Terraform configuration. This gives you both Formal's built-in version history and your team's standard code review workflow.
</Tip>

### Rolling Back to a Previous Version

You can restore any previous version of a policy:

1. Navigate to the policy in the web console
2. Open **Version History**
3. Select the version you want to restore
4. Click **Restore** to apply that version's code

The restored version becomes a **new version** (the version number increments), preserving the full history. For example, restoring version 3 when the current version is 5 creates version 6 with version 3's code.

<Tip>
  For critical rollbacks, you can also set the policy to **draft** or **dry-run** first, restore the version, verify it works as expected, then re-activate.
</Tip>

## Testing Policies

Formal provides several ways to test policies before they affect production traffic.

### 1. Syntax Validation

Formal validates your Rego code before saving. If there's a syntax error, the policy cannot be created or updated, and you'll receive a detailed error message.

Common validation errors:

| Error                | Cause                                        | Fix                                                       |
| -------------------- | -------------------------------------------- | --------------------------------------------------------- |
| `rego_parse_error`   | Invalid Rego syntax                          | Check for missing brackets, commas, or keywords           |
| `rego_compile_error` | Undefined variable or function               | Ensure all referenced variables exist in the policy scope |
| Wrong package name   | Package is not `formal.v2`                   | Use `package formal.v2` as the first line                 |
| Empty module         | Policy contains only the package declaration | Add at least one rule (session, request, or response)     |

### 2. Dry-Run Mode

Deploy a policy in **dry-run** mode to see what it *would* do without actually enforcing it:

1. Set the policy status to **Dry-run**
2. Monitor the [Logs](https://app.formal.ai/logs) page for policy trigger events
3. Dry-run events show what action the policy would have taken (block, mask, filter, etc.)
4. Once satisfied, switch to **Active**

<Tip>
  Dry-run policies are logged with full context, including the action that would have been taken and the reason. Use the Logs page to filter by policy name.
</Tip>

### 3. Impact Reports (Backtesting)

Test a policy against **historical log data** to understand its impact before activating it. Impact reports evaluate your policy code against real past traffic.

1. Navigate to [Policies](https://app.formal.ai/policies)
2. Select a policy or create a new one
3. Click **Impact Report**
4. Choose the time window to backtest against (up to 31 days)
5. Review the results: which queries would have been affected, and what actions would have been taken

<Warning>
  Impact reports require [Policy Evaluation Input Retention](/docs/guides/observability/logs#policy-evaluation-input-retention) to be enabled. Without retained inputs, there is no historical data to backtest against.
</Warning>

### 4. Stage-Based Rollout

You can disable policy evaluation at specific stages (session, request, or response) for a particular resource or connector. This lets you roll out a policy one stage at a time:

1. Deploy the policy in **active** mode
2. Disable evaluation at the stages you're not ready to test yet
3. Verify the enabled stage works correctly
4. Gradually enable the remaining stages

This is useful when a policy has logic across multiple stages and you want to validate each independently.

### 5. Local Testing with Desktop App

The [Formal Desktop App](/docs/guides/client-apps/desktop-app) lets you test policy evaluation locally. Connect to a resource through the Desktop App and run queries to see how policies evaluate in real time.

### Recommended Testing Workflow

1. **Write** the policy code
2. **Validate** syntax (automatic on save)
3. **Backtest** with an impact report against historical data
4. **Dry-run** against live traffic
5. **Review** logs for unexpected triggers
6. **Activate** when confident

## Policy Suspensions

Temporarily suspend a policy for specific users without deactivating it entirely.

### Time-Based Suspension

Grant a user temporary exemption from a policy for a defined duration (default: 24 hours). This is useful for break-glass scenarios where a user needs emergency access.

### One-Off Suspension

Grant a single-use exemption that expires after one matching request. This is useful for controlled exceptions — for example, allowing a single DELETE statement that would otherwise be blocked.

<Tip>
  Policy suspensions can be integrated with approval workflows. See [Workflows](/docs/guides/configuration/workflows) for examples of Slack-based suspension request flows.
</Tip>

## Performance Considerations

### How Policy Evaluation Works

* Policies are evaluated **in parallel** across a worker pool sized to the number of CPU cores on the Connector.
* Each policy is evaluated independently with its own context, so policies cannot interfere with each other.
* Results are merged after all policies complete, following the [conflict resolution rules](/docs/guides/policies/enforcement#rule-conflicts-and-precedence).

### Writing Performant Policies

<AccordionGroup>
  <Accordion title="Keep Rego rules simple" icon="bolt">
    Avoid deeply nested iterations or complex comprehensions over large datasets. Rego is optimized for set-based operations — prefer `in` checks over loops where possible.

    ```rego theme={null}
    # Preferred: set membership check
    session := {"action": "allow"} if {
      "admin" in input.user.groups
    }

    # Avoid: iterating and comparing
    session := {"action": "allow"} if {
      some group in input.user.groups
      group == "admin"
    }
    ```
  </Accordion>

  <Accordion title="Scope policies to specific resources" icon="crosshairs">
    Use `included_connectors` or `included_resources` to limit which traffic a policy evaluates against. Unscoped policies run on every request across all resources.

    ```rego theme={null}
    # Only evaluate for production databases
    included_resources := ["prod-postgres", "prod-mysql"]
    ```
  </Accordion>

  <Accordion title="Use the right evaluation stage" icon="layer-group">
    Choose the most efficient stage for your use case:

    * **Session**: Cheapest — evaluated once per connection
    * **Request**: Evaluated per query, but before data is fetched
    * **Response**: Most expensive — evaluated per query after data returns, with access to row/column data

    If you can enforce a rule at the session stage, prefer that over request or response.
  </Accordion>

  <Accordion title="Minimize Policy Data Loader payload size" icon="database">
    Data from Policy Data Loaders is fetched and loaded into the Connector's OPA context. Large datasets increase memory usage. Keep external data sets focused and small.
  </Accordion>

  <Accordion title="Consolidate related rules" icon="object-group">
    Multiple policies that check similar conditions create redundant evaluation work. Where possible, combine related rules into a single policy to reduce overhead.
  </Accordion>
</AccordionGroup>

### Scaling the Connector

If policy evaluation latency becomes a concern:

* **Scale vertically**: More CPU cores = more parallel policy evaluation workers.
* **Scale horizontally**: Add more Connector instances with [clustering](/docs/guides/core-concepts/connectors/clustering) enabled.
* **Monitor metrics**: Use the Connector's [health metrics](/docs/guides/core-concepts/connectors/health) to track evaluation latency.

## Troubleshooting

### Policy Not Triggering

<AccordionGroup>
  <Accordion title="Check policy status" icon="toggle-off">
    Ensure the policy is in **active** (or **dry-run**) status. Policies in **draft** status are never evaluated.
  </Accordion>

  <Accordion title="Check the evaluation stage" icon="layer-group">
    Verify your rule uses the correct stage name:

    * `session` — connection-time rules
    * `request` — before query execution
    * `response` — after data returns

    A `response` rule won't fire on connection, and a `session` rule won't have access to query data.
  </Accordion>

  <Accordion title="Check resource/connector scoping" icon="crosshairs">
    If the policy uses `included_resources`, `excluded_resources`, `included_connectors`, or `excluded_connectors`, verify that the target resource or connector is in scope.
  </Accordion>

  <Accordion title="Check input conditions" icon="magnifying-glass">
    Use dry-run mode or impact reports to verify what `input` data the policy receives. Common issues:

    * `input.user.groups` may be empty if the user isn't assigned to any groups
    * `input.resource.environment` may not be set if the resource has no environment tag
    * `input.query.statement_type` values are uppercase (e.g., `"SELECT"`, `"DELETE"`)
  </Accordion>

  <Accordion title="Check for conflicting policies" icon="shuffle">
    Another policy may be overriding yours. Remember: **block wins over allow**, and **higher privacy masking wins**. Check the Logs page for which policies were evaluated on a given request.
  </Accordion>
</AccordionGroup>

### Policy Blocking Unexpectedly

<AccordionGroup>
  <Accordion title="Check for default deny rules" icon="ban">
    If you have a `default session := {"action": "block", ...}` policy, it applies to **all** connections unless an explicit `allow` rule matches. Verify the allow conditions are correct.
  </Accordion>

  <Accordion title="Check precedence rules" icon="scale-balanced">
    When multiple policies apply, block always wins over allow. If any active policy returns `block`, the request is blocked regardless of other policies returning `allow`.
  </Accordion>

  <Accordion title="Use dry-run to debug" icon="flask">
    1. Set the problematic policy to **dry-run**
    2. Check the logs to see when it triggers
    3. Review the `input` data to understand why the conditions matched
    4. Adjust the policy logic and repeat
  </Accordion>

  <Accordion title="Suspend the policy temporarily" icon="pause">
    If a policy is blocking critical operations, use a [policy suspension](#policy-suspensions) to temporarily exempt specific users while you investigate.
  </Accordion>
</AccordionGroup>

### Masking Not Working as Expected

<AccordionGroup>
  <Accordion title="Check data labels" icon="tags">
    Masking policies that use `col.data_label` require [data discovery](/docs/guides/core-concepts/satellites) to have classified the columns. If labels aren't assigned, the masking condition won't match.
  </Accordion>

  <Accordion title="Check column paths" icon="sitemap">
    When matching by column path, use the full path format: `database.schema.table.column` (e.g., `main.public.users.email`).
  </Accordion>

  <Accordion title="Check privacy level precedence" icon="lock">
    If multiple masking policies target the same column, the one with the **highest privacy level** wins. See [Masking Types](/docs/guides/policies/enforcement#masking-types) for the privacy level hierarchy.
  </Accordion>

  <Accordion title="Check typesafe settings" icon="shield">
    If masking results in type errors in your application, set `"typesafe": true` with an appropriate `"typesafe_fallback"` (`"fallback_to_null"` or `"fallback_to_default"`) to maintain column data types.
  </Accordion>
</AccordionGroup>

### Common Rego Mistakes

| Mistake                                          | Symptom                          | Fix                                                            |
| ------------------------------------------------ | -------------------------------- | -------------------------------------------------------------- |
| Wrong package name                               | Policy never evaluates           | Use `package formal.v2`                                        |
| Missing `import future.keywords.if`              | Rego parse error                 | Add the import statement                                       |
| Using `=` instead of `==`                        | Assignment instead of comparison | Use `==` for equality checks inside rule bodies                |
| `input.query` in session stage                   | Undefined variable error         | Query data is only available in request and response stages    |
| Case-sensitive comparisons                       | Policy doesn't match             | SQL statement types are uppercase (`"SELECT"`, not `"select"`) |
| `not` without parentheses on complex expressions | Unexpected evaluation            | Use `not (expr)` for compound negations                        |
