Documentation

Everything you need to run SafeNode in production, call the evaluate API, and gate actions from any agent, app, or automation you build.

Getting started

In a few minutes you will: create an account, set up an organization and an agent, create an API key, and call POST /api/v1/evaluate so your software can ask “is this action allowed?” before it runs.

Use the sidebar links to jump between sections. When you are logged into the dashboard, open Documentation from the Help block in the sidebar for the same guide.

What is SafeNode?

SafeNode is a policy firewall for AI agents. Before your agent performs an action (send email, run a shell command, call an external API, write a file, etc.), you send a structured description of that action. SafeNode runs it through your organization’s policies and returns a decision: allow, warn, review, or deny. Your integration chooses how strictly to enforce each outcome.

Why use it? You get one place for rules, a full audit trail of evaluations, and the ability to tighten or loosen behavior without redeploying every client.

Product site: https://safenode.tech

Integrations overview

SafeNode is agent-agnostic. Any software that can make an HTTPS request can call POST /api/v1/evaluate before performing a gated action.

Custom agents
Workers, chatbots, and tool-use loops (e.g. built with the Claude Agent SDK or your own orchestration). Gate each tool call or write. See Custom agents.
Product backends
Server-side enforcement before CRM writes, billing actions, or admin APIs. See Seedling CRM for one example.
Scripts and automations
Cron jobs, CI steps, or internal tools that need a policy check and audit trail. Use curl, the JavaScript client SDK, or any HTTP client.

Pattern everywhere: evaluate first, act second. Your code branches on decision and logs trace_id.

Core concepts

Organization
Top-level tenant. Policies, agents, API keys, and evaluations belong to one organization.
Agent
A logical actor (e.g. “Support bot”, “Workflow worker”, a production service). API keys are tied to an agent so you can filter and audit by source.
API key
Secret used in Authorization: Bearer … (or X-Api-Key). Identifies the agent and organization for each evaluate call.
action_type
Short string naming the kind of action (e.g. tool.shell.exec, send_email). Your policies can match on this.
payload
JSON object with action-specific details (arguments, paths, recipients, model name, etc.).
context
JSON object with environment metadata (region, estimated cost, sensitivity, workspace id). Helps scoring and rules.
trace_id
Returned on every successful evaluation. Use it to correlate logs with a row in the Decision Feed.

Create your account

You need a SafeNode account to create organizations, agents, and API keys.

1

Go to the sign‑up page

Open Register (or “Register” in the site header).

2

Enter your email and password

Use a real email and a strong password.

3

Log in

After registering, log in via Log in when needed.

Create an organization

Everything in SafeNode lives under an organization. You can have more than one (e.g. staging vs production).

1

Open the dashboard

Click Dashboard in the header (or go to /admin).

2

Create or choose an organization

Create one with a name and slug, or use the organization switcher in the sidebar.

3

Remember the slug

The slug (e.g. acme) is a short identifier you may reference in context from your apps.

Add an agent

An agent is one thing that can perform actions. Create separate agents per integration or environment so evaluations stay attributable.

1

Go to Agents

In the dashboard sidebar, open Agents.

2

Create a new agent

Click “New agent”, set a name (e.g. “Claude worker” or “Production API”) and optional slug, then save.

3

One agent per logical actor

Prefer one agent per integration or environment rather than sharing one key everywhere.

Get your API key

Programs authenticate with an API key tied to an agent. The full key is shown only once when created—store it in a secret manager or environment variable.

1

Open API keys

In the sidebar, go to API keys (under the agent or organization, depending on your panel layout).

2

Create a key

Link it to the agent you created. Copy the key immediately.

3

Send it on every evaluate request

Use Authorization: Bearer YOUR_KEY or X-Api-Key: YOUR_KEY. Missing or invalid keys receive 401.

Call the API

Main endpoint: POST /api/v1/evaluate. Full URL is your app base URL plus /api/v1/evaluate (from APP_URL), e.g. https://safenode.tech/api/v1/evaluate.

Machine-readable spec: the full contract is published as OpenAPI 3.1 at /openapi.yaml. Use it to generate a typed client, load it into Postman or Insomnia, or hand it to a coding agent.

Headers

POST /api/v1/evaluate
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

Body (required fields)

  • action_type (string, required)
  • payload (object, optional—omit or send {})
  • context (object, optional)
  • agent_id (string, optional—usually omitted when the API key already identifies one agent)

Minimal example

{
  "action_type": "send_email",
  "payload": {
    "to": "user@example.com",
    "subject": "Your order",
    "body": "Order #123 has shipped."
  }
}

With context (recommended)

{
  "action_type": "call_model",
  "payload": { "model": "gpt-4", "tokens": 500 },
  "context": {
    "region": "eu-west-1",
    "vendor_id": "openai",
    "cost_estimate": 0.02
  }
}

Context key names are load-bearing. Rules read specific keys; anything else is inert unless a context_match rule targets it by name. Use these:

KeyAliasRead by
vendor_idvendor_slugblocked_vendor
cost_estimatespendspend_threshold
regioncloud_regionunapproved_region_cloud
geo_countrycountry_codeunapproved_region_geo
org_idcontext_match (your tenant id, not a SafeNode org selector)

A common mistake is sending cost_usd for spend gating. No rule reads it, so the gate silently never fires. Use cost_estimate.

Understand the response

On success, JSON includes:

  • decisionallow, warn, review, or deny
  • trace_id — UUID for this evaluation (no prefix)
  • impact_score / risk_score — numeric scores on a 0–100 scale, to two decimal places
  • matched_policies — soft rules that matched, as [{"rule_id": "spend_threshold"}]
  • reasons — human-readable strings
  • alternatives — suggested safer options; never empty

Three details that surprise people:

  • Scores are 0–100, not 0–1. Decision bands default to allow ≤ 30, warn ≤ 50, review ≤ 75, above that deny. A band applies only when both scores fall under its ceiling, so do not threshold on impact_score alone.
  • alternatives is never an empty array. A generic suggestion is appended when nothing more specific applies — including on allow. A non-empty alternatives is not evidence that something was wrong.
  • matched_policies is empty on hard-rule denials. It is populated from soft rules only. When a hard rule denies, use reasons for attribution.

Suggested enforcement:

  • deny — do not perform the action.
  • review — block until a human approves (or your product policy allows auto-escalation).
  • warn — allow only if your product policy says warnings are acceptable; log and surface to the user.
  • allow — proceed.
{
  "decision": "allow",
  "trace_id": "9f1c2b7e-3d4a-4c8b-9e10-5a6f7b8c9d0e",
  "impact_score": 12.5,
  "risk_score": 6.25,
  "matched_policies": [],
  "reasons": [],
  "alternatives": [
    {
      "type": "general",
      "description": "Review the policy rules and adjust the request or contact your administrator."
    }
  ]
}

Rate limits & errors

The evaluate endpoint is rate limited to 60 requests per minute per API key by default (configurable on the server). Exceeding it returns 429 Too Many Requests.

Two different things return 429

This is the most important error-handling detail on this page. Rate limiting and monthly quota exhaustion share a status code but mean opposite things:

ThrottledMonthly cap reached
MeaningToo many requests this minutePlan's monthly evaluation allowance is spent
Retry-After headerpresentabsent
evaluations_cap in bodyabsentpresent
Retry?Yes, after Retry-AfterNo — not until the next billing month

Discriminate on evaluations_cap. Retrying a quota 429 with backoff will keep failing for the rest of the month; upgrade the plan or raise the cap instead.

{
  "message": "Monthly evaluation limit reached. You have used 1000 of 1000 evaluations for this month.",
  "evaluations_used": 1000,
  "evaluations_cap": 1000
}

Rate limit headers follow Laravel defaults: X-RateLimit-Limit and X-RateLimit-Remaining on every response, plus Retry-After and X-RateLimit-Reset on a throttle 429.

Other responses

  • 401 — missing, invalid, expired, or unbound API key.
  • 422 — validation failure or oversize payload. These have different body shapes: a validation failure includes an errors object, the size rejection does not. payload and context are each capped at 256 KiB of JSON independently, not as a combined budget.
  • 404 — the key resolved to an organization that no longer exists.
  • 500 — server error (rare); check service status and logs.

Retries and timeouts

Retry with backoff on transient 5xx and on throttle 429 only.

Do not retry a request that timed out. Every evaluate call is a write — it creates an evaluation record and counts against your monthly allowance — so a timed-out call may already have been recorded. Retrying it double-counts your usage, duplicates the Decision Feed, and doubles worst-case latency on a call that sits in front of a user action. The official SDKs never retry timeouts, by design.

If SafeNode is unreachable

You choose. The official SDKs expose this as on_unavailable and default to fail-open: the action proceeds and the result is flagged degraded, because a policy service that takes production down during its own outage is worse than an unpoliced window. Degraded results carry no trace_id, so they can never be miscounted as real policy decisions.

That default suits audit and read-only paths. For anything that moves money, deletes data, or sends mail externally, prefer fail-closed. Splitting your client into two — one fail-open for reads, one fail-closed for writes — is usually the right shape.

Using the dashboard

  • Decision Feed — recent evaluations; filter by decision, agent, or time; open a row for full detail and trace_id.
  • Stats — volume and outcome breakdowns.
  • Policies — active policy version, hard rules, soft rules, weights, and decision bands.
  • Overrides — manual allow/deny on past evaluations (audited).

For a short marketing overview of the product, you can still read How it works on the homepage—this documentation is the operational source of truth.

Custom agents

Whether you use the Claude Agent SDK, LangChain, a hand-rolled tool loop, or a product-specific worker, the integration shape is the same: before a tool runs or a write is persisted, call SafeNode and honor the decision.

1

Agent + API key in SafeNode

Create one agent per integration or environment. Store SAFENODE_API_KEY and SAFENODE_BASE_URL server-side — never in client-side code or a public repo.

2

Hook the tool / action boundary

In your agent runtime, intercept each tool call (or each external write). Map the tool name to an action_type, build payload and context, then call evaluate.

3

Use stable action_type names

Pick a namespace per agent, e.g. tool.send_email, tool.shell.exec, tool.fs.write, so policies can target one integration without affecting others.

4

Enforce the decision

deny and (usually) review block the tool. warn is your product choice. Log trace_id on every path.

Example: Claude Agent SDK tool hook

Illustrative pattern — adapt to your SDK version and tool registry. The important part is gating before side effects:

import { SafeNodeClient } from "./scripts/safenode-client-sdk.mjs";

const safenode = new SafeNodeClient({
  baseUrl: process.env.SAFENODE_BASE_URL,
  apiKey: process.env.SAFENODE_API_KEY,
});

async function runToolWithPolicy(toolName, toolInput, sessionContext) {
  const gate = await safenode.gateAction(
    {
      actionType: `tool.${toolName}`,
      payload: {
        summary: `Run tool: ${toolName}`,
        fields: toolInput,
      },
      context: {
        source: "claude-agent",
        user_id: sessionContext.userId,
        session_id: sessionContext.sessionId,
      },
    },
    { allowWarn: false, allowReview: false }
  );

  if (!gate.allowed) {
    return {
      type: "tool_result",
      content: `Blocked by policy: ${gate.reason} (trace ${gate.traceId})`,
      is_error: true,
    };
  }

  return executeToolImpl(toolName, toolInput);
}

Attach runToolWithPolicy wherever your agent dispatches tool calls. For read-only tools you may choose fail-open when SafeNode is down; for writes, prefer fail-closed.

Optional helper: the JavaScript client SDK in this repo (scripts/safenode-client-sdk.mjs) wraps evaluate with timeouts, retries, and gateAction() defaults.

Python SDK

The official Python client. MIT licensed, one dependency (httpx), no telemetry.

pip install safenode-sdk
import os
from safenode_sdk import SafeNode

sn = SafeNode(api_key=os.environ["SAFENODE_API_KEY"])

result = sn.evaluate(
    "send_email",
    payload={"to": "customer@example.com", "subject": "Your refund"},
    context={"vendor_id": "sendgrid", "cost_estimate": 0.01},
)

if result.denied:
    raise RuntimeError(f"Blocked: {result.reasons} (trace {result.trace_id})")

The import name is safenode_sdk; the package name on PyPI is safenode-sdk.

  • SafeNode and AsyncSafeNode with an identical surface.
  • guard() context manager and @guarded() decorator that raise PolicyDenied instead of returning a decision.
  • Three fail modes via on_unavailable, defaulting to fail-open with results flagged degraded.
  • Client-side redaction before anything is sent, plus a metadata_only mode that sends no payload values at all. build_request() shows you exactly what would leave your process, without sending it.
  • Separate RateLimitError and QuotaExceededError, so the two kinds of 429 are handled correctly for you.
  • Non-blocking evaluate_async() for audit-and-alert when you cannot afford a round trip.

Timeouts default to 2000 ms total and 500 ms connect, and timed-out calls are never retried — see Retries and timeouts for why.

JavaScript client SDK

Small reference ES module in the SafeNode repo (no npm package required to start). A published npm package is in progress; until then:

  • Path: scripts/safenode-client-sdk.mjs
  • Exports: SafeNodeClient, gateWithSafeNode
  • Features: Bearer auth, request timeout, exponential backoff retries on transient failures, evaluate() and gateAction() with fail-closed defaults and optional read-only fail-open.
import { SafeNodeClient } from "./scripts/safenode-client-sdk.mjs";

const client = new SafeNodeClient({
  baseUrl: process.env.SAFENODE_BASE_URL,
  apiKey: process.env.SAFENODE_API_KEY,
  timeoutMs: 5000,
  maxRetries: 2,
});

const gate = await client.gateAction(
  {
    actionType: "tool.send_email",
    payload: { to: "user@example.com", subject: "Hello" },
    context: { environment: "production", sensitivity: "internal" },
  },
  { allowWarn: true, allowReview: false, failOpenForReadOnly: true, isReadOnlyAction: false }
);

if (!gate.allowed) {
  // block action; gate.reason, gate.traceId, gate.response
}

Copy the file into your agent project or vendor it from this repository.

Seedling CRM integration

Seedling CRM can call SafeNode before gated writes (tasks, customers, calendar, etc.). If you connect an external agent to Seedling (see Seedling’s API help), you need your own SafeNode organization, agent, API key, and policy — Seedling does not configure that for you.

Who needs a SafeNode policy?

  • Seedling app users only — usually no. The Seedling platform runs server-side enforcement with its own API key and policy. Your writes are gated by that platform policy.
  • Agent / automation ownersyes. When you connect Cursor, a custom agent, or a worker to Seedling, create SafeNode credentials and a policy on your SafeNode org, then paste the API key into Seedling’s agent settings.

Your API key determines which SafeNode organization enforces policy. Do not set a separate org id env var — only SAFENODE_BASE_URL and SAFENODE_API_KEY.

Setup checklist

  1. Register at SafeNode and create an organization.
  2. Create an agent named Seedling with slug seedling.
  3. Create an API key on that agent. Store it server-side as SAFENODE_API_KEY.
  4. Policies → New policy → add a version with the starter JSON below → Set active.
  5. In Seedling, paste the key where agent / API integration settings ask for SafeNode credentials.

Starter policy (Seedling writes)

Denies destructive delete_* actions, routes large bulk task creates to review, and optionally warns on off-hours customer creation. Tune timezones and thresholds for your org.

{
    "version": 1,
    "hard_rules": [
        {
            "id": "deny-deletes",
            "type": "action_type_match",
            "params": {
                "patterns": [
                    "delete_*"
                ],
                "message": "Destructive delete actions are denied by default."
            }
        }
    ],
    "soft_rules": [
        {
            "id": "bulk-task-review",
            "type": "action_type_context_threshold",
            "params": {
                "action_types": [
                    "bulk_create_task"
                ],
                "context_key": "count",
                "gt": 5,
                "message": "Bulk task creation exceeds 5 items \u2014 route to human review."
            },
            "weight": 100
        },
        {
            "id": "customer-business-hours",
            "type": "action_type_business_hours",
            "params": {
                "action_types": [
                    "create_customer"
                ],
                "timezone": "America/Los_Angeles",
                "start_hour": 9,
                "end_hour": 17,
                "weekdays_only": true,
                "message": "Creating customers outside business hours should be reviewed."
            },
            "weight": 50
        }
    ],
    "weights": {
        "privacy": 25,
        "carbon": 15,
        "cost": 20,
        "trust": 20,
        "policy_fit": 20
    },
    "default_decision": "allow",
    "decision_bands": {
        "allow_max": 30,
        "warn_max": 50,
        "review_max": 75
    }
}

Rule types used: action_type_match, action_type_context_threshold, action_type_business_hours. For allowlists per Seedling tenant inside one policy, add context_match on context.org_id (the Seedling tenant id — not your SafeNode org id).

Action catalog (source of truth for action_type strings): Seedling repo docs/seedling-api/safenode-action-catalog.md.

Full example

  1. Register and create organization + agent + API key.
  2. Set SAFENODE_API_KEY in your environment.
  3. Run:
curl -X POST https://safenode.tech/api/v1/evaluate \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SAFENODE_API_KEY" \
  -d '{
    "action_type": "send_email",
    "payload": { "to": "user@example.com", "subject": "Hi", "body": "Hello" },
    "context": { "region": "us-east-1" }
  }'

Inspect decision and trace_id, then find the evaluation in the Decision Feed.

Site admin (production)

Site admins can access the separate Super Admin panel at /super-admin (manage users, site-wide settings). This is independent of organization owner / admin roles inside the main dashboard.

On the server, SSH to the app directory and run:

php artisan safenode:make-site-admin your@email.com

If the user does not exist yet:

php artisan safenode:make-site-admin your@email.com --create

The command will prompt for name, password, and optionally an organization slug. Afterward that user can open /super-admin when logged in.