> ## 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.

# Hooks

> Extend policy evaluation with external data and custom logic

Hooks extend policy evaluation with data or logic unavailable in the standard policy input. Each hook receives the current policy input and returns JSON for Rego.

Common use cases include:

* Fetching a risk score from an internal service
* Checking temporary approvals or maintenance windows
* Adding business context from an existing API
* Normalizing an external response into a stable object for policies

Hooks run in the access path. Keep their execution time and returned data as small as possible.

## Writing Hooks

Hooks are written in JavaScript or TypeScript. The source must provide a default exported function, which may be synchronous or asynchronous.

```ts theme={null}
type HookCache = {
  get(key: string): unknown;
  set(key: string, value: unknown, ttlSeconds: number): void;
};

export default async function hook(
  input: Readonly<Record<string, unknown>>,
  env: Readonly<Record<string, string>>,
  cache: HookCache,
): Promise<unknown> {
  return {};
}
```

The function accepts the following arguments:

| Argument | Description                                                           |
| -------- | --------------------------------------------------------------------- |
| `input`  | Policy input for the current evaluation stage                         |
| `env`    | Allowed environment variables that are set on the evaluating endpoint |
| `cache`  | Per-hook cache for JSON-compatible values                             |

Only `input` is required. Hooks that do not use environment variables or caching may omit the other arguments.

The return value must be representable as JSON. Objects, arrays, strings, numbers, booleans, and `null` are supported. The top-level value cannot be `undefined`, a function, or a symbol. Circular objects and `bigint` values are also unsupported.

Imports are not supported. All required logic must be included in the hook source.

### Status

| Status     | Behavior                                                    |
| ---------- | ----------------------------------------------------------- |
| **Draft**  | Saved in Formal, but unavailable during policy evaluation   |
| **Active** | Available to policies evaluated by Connectors and Endpoints |

Use draft status while developing a hook. Activate it before creating or updating a policy that references it.

## Policy Evaluation

Policies reference hook results through `input.hooks.<name>`. This policy blocks a request when the `risk` hook returns a score of 5 or greater:

```rego theme={null}
package formal.v2

import future.keywords.if

request := {
  "action": "block",
  "type": "block_with_formal_message",
  "reason": "Request risk is too high"
} if {
  input.hooks.risk.score >= 5
}
```

Formal first evaluates the parts of each policy that do not depend on hooks. It then invokes only the hooks required to complete the policy decision.

Each required hook receives the policy input for the current session, request, or response stage. Formal adds the returned value to `input.hooks`. It then completes the policy evaluation. Formal invokes a shared hook once when several applicable policies reference it.

Hook references must use a fixed name. Both `input.hooks.risk` and `input.hooks["risk"]` are valid. Dynamic references such as `input.hooks[hook_name]` are not supported.

<Note>
  A policy can only reference an active hook. Formal rejects references to
  missing or draft hooks when validating the policy.
</Note>

## Complete Example

The following hook requests a risk score and caches the result for 60 seconds:

```ts theme={null}
type PolicyInput = Readonly<{
  user: Readonly<{ email?: string }>;
  resource: Readonly<{ id: string }>;
}>;

type HookCache = {
  get(key: string): unknown;
  set(key: string, value: unknown, ttlSeconds: number): void;
};

export default async function hook(
  input: PolicyInput,
  env: Readonly<Record<string, string>>,
  cache: HookCache,
): Promise<unknown> {
  const cacheKey = `${input.user.email ?? "unknown"}:${input.resource.id}`;
  const cached = cache.get(cacheKey);

  if (cached !== undefined) {
    return cached;
  }

  const token = env["RISK_API_TOKEN"];
  if (!token) {
    throw new Error("RISK_API_TOKEN is not set");
  }

  const response = await fetch("https://risk.example.com/v1/score", {
    method: "POST",
    headers: {
      authorization: `Bearer ${token}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      user: input.user.email,
      resource: input.resource.id,
    }),
  });

  if (!response.ok) {
    throw new Error(`Risk service returned ${response.status}`);
  }

  const result = await response.json();
  cache.set(cacheKey, result, 60);
  return result;
}
```

This hook requires the following configuration:

| Setting                       | Value              |
| ----------------------------- | ------------------ |
| Allowed network hosts         | `risk.example.com` |
| Allowed environment variables | `RISK_API_TOKEN`   |
| Status                        | `active`           |

## Network Access

Network access is denied by default. A hook can use `fetch` only for destinations included in its allowed network hosts.

The allowlist accepts hostnames, IP addresses, and IP ranges in CIDR notation. Enter the destination without a scheme, path, or port. An allowed destination permits connections to all ports on that host or range.

For example, allow `risk.example.com` rather than `https://risk.example.com/v1/score`.

## Environment Variables

The `env` argument contains only variables named in the hook's allowed environment variables. Formal omits allowed variables that are not set on the evaluating endpoint.

No environment variables are available by default. Required variables must be configured on every Connector or Endpoint that may evaluate the hook.

<Warning>
  A hook can send an allowed environment variable to an allowed network host.
  Only grant both capabilities when the hook requires them.
</Warning>

## Caching

The `cache` argument stores JSON-compatible values between evaluations of the same hook. Each hook's cache is isolated from other hooks.

```ts theme={null}
const value: unknown = cache.get("key");
cache.set("key", { enabled: true }, 60);
```

`cache.get(key)` returns the stored value. It returns `undefined` when the key is missing or expired. `cache.set(key, value, ttlSeconds)` stores the value for the specified number of seconds. Writes are immediately visible to later reads during the same evaluation.

To share cached values across multiple instances of the same Connector, enable [clustering](/docs/guides/core-concepts/connectors/clustering). Replication between instances is eventually consistent: an evaluation sees its own writes, but other Connector instances may not see them immediately.

Cache availability and persistence failures do not fail an otherwise successful hook evaluations. Writes are applied after successful execution and failures are logged.

<Note>
  Caching is in-memory only and does not persist across Connector or Endpoint restarts. Cache entries may be unavailable or expire at any time. Treat the cache as an optimization rather than an authoritative data source.
</Note>

## Limits

### Execution Limits

| Limit                 | Value                                          |
| --------------------- | ---------------------------------------------- |
| Default timeout       | 5 seconds                                      |
| Configurable timeout  | Up to 60 seconds                               |
| Serialized output     | 1 MiB per evaluation                           |
| Network access        | Denied by default; restricted to allowed hosts |
| Environment variables | None by default; restricted to allowed names   |
| Imports               | Not supported                                  |
| Filesystem access     | Not available                                  |

### Cache Limits

| Limit                 | Value                              |
| --------------------- | ---------------------------------- |
| Key size              | 256 bytes                          |
| Serialized value size | 4 KiB                              |
| Entries               | 1,024 per hook                     |
| Writes                | 1,024 distinct keys per evaluation |
| Total size            | 4 MiB per hook                     |
| Time to live          | Up to 24 hours                     |

Output and cache value limits use UTF-8 byte counts after JSON serialization. Cache key limits also use UTF-8 byte counts.

## Errors

A hook evaluation fails when the hook:

* Exceeds its configured timeout
* Throws an error or returns a rejected promise
* Attempts a network request to a destination that is not allowed
* Returns a value that cannot be represented as JSON
* Returns more than 1 MiB of serialized JSON
* Exceeds a cache limit

When a required hook fails, the policy evaluation returns an error. Formal does not continue with an absent or partial hook result.

Use explicit timeouts, handle upstream errors, and keep external dependencies reliable.

## Next Steps

<CardGroup cols={2}>
  <Card title="Policy Evaluation" icon="microscope" href="/docs/guides/policies/evaluation">
    Review the input available at each policy stage
  </Card>

  <Card title="Policy Examples" icon="code" href="/docs/guides/policies/examples">
    Explore common policy patterns
  </Card>
</CardGroup>
