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

# Build custom quality gates for Pecta engines

> Implement the Gate interface to add domain-specific pass/fail checks to any engine, alongside or instead of Pecta's built-in gates.

Every built-in gate in Pecta is an object that satisfies the `Gate` interface. You can implement the same interface yourself to add checks that are specific to your domain — for example, enforcing a proprietary response schema, calling an internal moderation API, or checking that a bid came from an allowlisted seat.

Custom gates compose with built-in gates in `createEngine` the same way built-in gates do. The engine handles parallelism, the timeout budget, fail-fast signalling, and result assembly automatically.

## The `Gate` interface

```typescript theme={null}
export interface Gate {
  name: string;
  run: GateRun;
}
```

`name` is the string recorded in `result.gates` and used for duplicate detection. It must be unique across all gates in an engine.

## The `GateRun` signature

```typescript theme={null}
export type GateRun = (
  ctx: EvaluationContext,
  signal: AbortSignal
) => Promise<GateOutcome> | GateOutcome;
```

`run` receives the full evaluation context and an `AbortSignal` from the engine's shared `AbortController`. It can return a `GateOutcome` synchronously or as a `Promise`.

## The `GateOutcome` type

```typescript theme={null}
export type GateOutcome = {
  passed: boolean;
  reason?: string;
  skipped?: boolean;
  details?: unknown;
}
```

| Field     | Description                                                                                                                 |
| --------- | --------------------------------------------------------------------------------------------------------------------------- |
| `passed`  | `true` if the check passed, `false` if it failed.                                                                           |
| `reason`  | Human-readable explanation of why the gate failed or was skipped. Optional on success.                                      |
| `skipped` | Set to `true` to opt out of evaluation for this call. Skipped gates are treated as passing.                                 |
| `details` | Any structured data useful for debugging — Zod issues, matched tokens, API response fragments. Never include user payloads. |

The engine fills in `name` and `latency_ms` automatically. You do not need to set them in your `run` function.

## A complete custom gate

This gate verifies that the output's `bid_id` field appears in an in-memory allowlist. It checks `signal.aborted` before doing any work, which is the correct pattern for gates that might do expensive I/O:

```typescript theme={null}
import type { Gate, EvaluationContext, GateOutcome } from "@pecta/core";

const ALLOWED_SEATS = new Set(["seat-001", "seat-002", "seat-099"]);

const seatAllowlistGate: Gate = {
  name: "seat.allowlist",
  run(ctx: EvaluationContext, signal: AbortSignal): GateOutcome {
    // Always check signal.aborted before expensive work.
    if (signal.aborted) {
      return { passed: false, reason: "aborted" };
    }

    const output = ctx.output as Record<string, unknown>;
    const seat = typeof output?.seat === "string" ? output.seat : undefined;

    if (seat === undefined) {
      return { passed: false, reason: "missing seat field in output" };
    }

    if (!ALLOWED_SEATS.has(seat)) {
      return {
        passed: false,
        reason: `seat "${seat}" is not in the allowlist`,
        details: { seat, allowed: [...ALLOWED_SEATS] },
      };
    }

    return { passed: true };
  },
};
```

## Using an async gate

When your gate needs to call an external service, return a `Promise`. The engine awaits it and applies the timeout budget the same way as synchronous gates:

```typescript theme={null}
import type { Gate } from "@pecta/core";

const moderationGate: Gate = {
  name: "moderation.api",
  async run(ctx, signal) {
    if (signal.aborted) return { passed: false, reason: "aborted" };

    const response = await fetch("https://internal.example.com/moderate", {
      method: "POST",
      signal, // forward the AbortSignal so the fetch is cancelled on timeout
      body: JSON.stringify({ text: ctx.output }),
      headers: { "Content-Type": "application/json" },
    });

    const { safe } = await response.json() as { safe: boolean };
    return safe
      ? { passed: true }
      : { passed: false, reason: "moderation API flagged output" };
  },
};
```

<Warning>
  Forward the `signal` to any `fetch` or other cancellable API call inside your gate. If you do not, a network request will keep running even after the engine's timeout fires, and the `Promise` will not resolve until the network responds — causing the engine to wait beyond its timeout budget.
</Warning>

## Passing your gate to `createEngine`

Custom gates go in the same `gates` array as built-in gates:

```typescript theme={null}
import { createEngine, gates } from "@pecta/core";

const engine = createEngine({
  gates: [
    gates.content(),
    gates.pii(),
    seatAllowlistGate,       // custom gate
    moderationGate,          // another custom gate
  ],
  timeout: 50,
});

const result = await engine.evaluate({
  agent_id: "dsp-bidder",
  output: { seat: "seat-999", text: "Buy now!" },
});
```

## What the engine fills in automatically

You only return `GateOutcome` from `run`. The engine wraps it into a full `GateResult` before adding it to `result.gates`:

```typescript theme={null}
// What you return from run:
{ passed: false, reason: 'seat "seat-999" is not in the allowlist' }

// What appears in result.gates:
{
  name: "seat.allowlist",          // from gate.name
  passed: false,
  reason: 'seat "seat-999" is not in the allowlist',
  latency_ms: 0.23                 // measured by the engine
}
```

## Skipping conditionally

Return `{ passed: true, skipped: true }` when your gate cannot or should not evaluate this particular context. Skipped results appear in `result.gates` but do not cause `result.passed` to be `false`:

```typescript theme={null}
run(ctx, signal): GateOutcome {
  if (ctx.tool !== "bidder.respond") {
    return { passed: true, skipped: true, reason: "not a bid response" };
  }
  // ...rest of logic
}
```

<Info>
  The `GateRun` type is exported from `@pecta/core` alongside `Gate`, `GateOutcome`, and `EvaluationContext`. Import all the types you need directly from the package — you do not need to import from sub-paths.
</Info>
