Seal

Quickstart

Gate a risky agent action behind a human approval in five minutes — install the SDK, call gate.require(), approve in Slack.

Seal puts a human in the loop for the actions your agent should not take alone. Your agent asks for approval, a policy decides whether a human is needed, and gate.require() blocks until someone approves or rejects in Slack. Every decision is written to a tamper-evident audit chain.

This quickstart takes you from zero to your first Slack approval.

1. Install the SDK

npm install @seal-dev/sdk

The SDK is ESM + CJS and ships its own types. It talks to the REST API over fetch, so it runs on Node 18+ or any runtime with a global fetch.

2. Create a gate

You need two things: an API key with the approvals:write and approvals:read scopes, and the base URL of your Seal API.

gate.ts
import { createGate } from "@seal-dev/sdk";

export const gate = createGate({
  apiKey: process.env.SEAL_API_KEY!,
  baseUrl: process.env.SEAL_BASE_URL!, // e.g. https://api.your-org.com
});
.env
SEAL_API_KEY=sk_live_...
SEAL_BASE_URL=https://api.your-org.com

3. Require approval before acting

Wrap the risky action. require() creates the approval, then long-polls until it settles. It resolves with the approved request and throws a typed error on rejection, expiry, or timeout — so the happy path is a straight line.

transfer.ts
import { ApprovalRejectedError } from "@seal-dev/sdk";

import { gate } from "./gate.js";

async function wireTransfer(to: string, amount: number) {
  try {
    await gate.require({
      action: "wire.transfer",
      policyKey: "payments.high_value",
      payload: { to, amount },
      context: { reason: "Vendor invoice #4471 auto-paid by finance agent" },
    });

    // Reached only after a human approves.
    await bank.send({ to, amount });
  } catch (err) {
    if (err instanceof ApprovalRejectedError) {
      console.log("Rejected:", err.feedback);
      return;
    }
    throw err;
  }
}
  • action — what the agent wants to do.
  • policyKey — which policy decides the verdict.
  • payload — the data the policy evaluates and the reviewer sees.
  • context — the agent's reasoning, shown to the reviewer (never evaluated).

4. Wire the policy

A policy attached to payments.high_value decides whether this action needs a human, and who approves. The example below auto-allows small transfers and routes anything over 1000 to Slack:

{
  "match": { "action": "wire.transfer" },
  "rules": [
    {
      "when": [{ "field": "payload.amount", "op": "gt", "value": 1000 }],
      "then": "require_approval"
    },
    { "when": [], "then": "allow" }
  ],
  "approvers": {
    "channel": "slack",
    "users": ["U0123ABC"],
    "teams": [],
    "escalation": [],
    "timeoutSeconds": 900,
    "onTimeout": "deny"
  }
}

If no policy matches, Seal fails closed: the verdict is require_approval, never allow. You can never accidentally ship a permissive default.

Policies are managed in the dashboard. For a local dev instance, pnpm db:seed installs exactly this policy under the key payments.high_value.

5. Approve in Slack

Call wireTransfer("acct_999", 5000). Because 5000 > 1000, the policy returns require_approval, a card appears in Slack with Approve / Reject, and your require() call is still awaiting. Click Approve — the promise resolves and the transfer runs. Reject it and ApprovalRejectedError is thrown with the reviewer's feedback.

That is the whole loop. Next:

On this page