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/sdkThe 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.
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
});SEAL_API_KEY=sk_live_...
SEAL_BASE_URL=https://api.your-org.com3. 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.
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:
- Policy authoring — match rules, condition operators, escalation, timeouts.
- SDK reference —
requireoptions,resume, error types. - Webhooks — react to decisions out of band.
- Audit export — pull the tamper-evident log for compliance.