JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 22
  • Score
    100M100P100Q97752F
  • License MIT

Official SDK for the Runplane control plane - runtime governance for AI agent actions

Package Exports

  • @runplane/runplane-sdk

Readme

@runplane/runplane-sdk

Runplane SDK is the fastest way to add a control layer in front of real-world actions executed by AI agents and application workflows.

Runplane does not execute actions. Runplane decides whether an action is allowed to execute.

The SDK is a thin wrapper over the Runplane Guard API. All decisions are made in the Gateway.

Core concept

Every protected action is intercepted before execution and must receive a decision first.

Decision Behavior
ALLOW Execution proceeds immediately
BLOCK Execution is stopped
REQUIRE_APPROVAL Execution pauses until a human approves or denies

No protected action executes without first passing through Runplane.

Why Runplane

AI agents and automations can trigger real-world side effects: deleting data, transferring funds, modifying infrastructure, sending messages, or changing production systems.

Runplane adds a runtime control layer before those actions execute.

Use it to:

  • Prevent destructive actions
  • Require human approval for risky operations
  • Enforce policies in real time
  • Add a verifiable decision layer in front of production actions

Without Runplane, actions run immediately. With Runplane, every action is checked first.

Get started

Create an account and get an API key:

👉 https://runplane.ai/auth/sign-up?mode=developer

Includes a free trial and limited free usage for early testing.

A valid API key is required.

Installation

npm install @runplane/runplane-sdk

Quick start

If your project was created with npm init -y, it may default to CommonJS. To use the import example below, set this in your package.json:

{
  "type": "module"
}

Then create index.js:

import { Shield } from "@runplane/runplane-sdk";

const runplane = new Shield({
  baseUrl: "https://runplane.ai",
  apiKey: process.env.RUNPLANE_API_KEY,
});

async function main() {
  const result = await runplane.guard(
    "delete_record",
    "users-database",
    { recordId: "user_123" },
    async () => {
      console.log("Executing delete...");
      return { deleted: true };
    }
  );

  console.log("Result:", result);
}

main();

Option B: CommonJS

If you want to stay on CommonJS, use require instead:

const { Shield } = require("@runplane/runplane-sdk");

const runplane = new Shield({
  baseUrl: "https://runplane.ai",
  apiKey: process.env.RUNPLANE_API_KEY,
});

async function main() {
  const result = await runplane.guard(
    "delete_record",
    "users-database",
    { recordId: "user_123" },
    async () => {
      console.log("Executing delete...");
      return { deleted: true };
    }
  );

  console.log("Result:", result);
}

main();

Set your API key

macOS / Linux

export RUNPLANE_API_KEY=your_api_key_here
node index.js

Windows CMD

set RUNPLANE_API_KEY=your_api_key_here
node index.js

Windows PowerShell

$env:RUNPLANE_API_KEY="your_api_key_here"
node index.js

What happens when you call guard()

ALLOW

The callback executes immediately.

Expected output:

Executing delete...
Result: { deleted: true }

BLOCK

The callback never runs. The SDK throws ShieldError with code "BLOCKED".

import { ShieldError } from "@runplane/runplane-sdk";

try {
  await runplane.guard("dangerous_action", "production", {}, async () => {
    // never executes
  });
} catch (err) {
  if (err instanceof ShieldError && err.code === "BLOCKED") {
    console.log("Action blocked:", err.message);
  }
}

REQUIRE_APPROVAL

The SDK waits for human approval before running the callback.

What happens internally:

  1. The SDK sends the action to the Guard API.
  2. The API returns REQUIRE_APPROVAL and an approvalId.
  3. The SDK polls the approval endpoint until the request is resolved.
  4. If approved, the callback executes.
  5. If denied, the SDK throws ShieldError with code "DENIED".
  6. If approval times out, the final behavior depends on configuration.

Where approvals happen

Pending approvals are reviewed in the Runplane dashboard under Approvals.

If your script seems to be waiting and no callback output appears yet, the action is likely pending human review.

First-run diagnostic example

This version is useful for verifying whether your script is waiting for approval, blocked, or timing out.

import { Shield, ShieldError } from "@runplane/runplane-sdk";

console.log("API key loaded:", !!process.env.RUNPLANE_API_KEY);
console.log("Starting guarded action...");

const runplane = new Shield({
  baseUrl: "https://runplane.ai",
  apiKey: process.env.RUNPLANE_API_KEY,
  approvalTimeoutMs: 15000,
  approvalPollIntervalMs: 2000,
});

async function main() {
  try {
    const result = await runplane.guard(
      "delete_record",
      "users-database",
      { recordId: "user_123" },
      async () => {
        console.log("Callback is executing now...");
        return { deleted: true };
      }
    );

    console.log("Final result:", result);
  } catch (err) {
    if (err instanceof ShieldError) {
      console.log("ShieldError code:", err.code);
      console.log("ShieldError message:", err.message);
      console.log("Request ID:", err.requestId);
    } else {
      console.error("Unknown error:", err);
    }
  }
}

main();

Configuration

const runplane = new Shield({
  baseUrl: "https://your-runplane-instance.com",
  apiKey: process.env.RUNPLANE_API_KEY,

  // Request timeout in milliseconds
  timeoutMs: 3000,

  // "closed" blocks on failures, "open" allows on failures
  failMode: "closed",

  // Maximum time to wait for approval
  approvalTimeoutMs: 300000,

  // Initial polling interval for approval checks
  approvalPollIntervalMs: 2000,
});

Approval flow

SDK                          Guard API                    Human
 |                              |                           |
 |-- POST /api/v1/guard ------->|                           |
 |<-- { decision: REQUIRE_APPROVAL, approvalId: "..." } ---|
 |                              |                           |
 |-- GET /api/v1/approvals/{id} (poll) -------------------->|
 |<-- { status: "pending" } ---|                           |
 |                              |                           |
 |          ... waiting and polling with backoff ...       |
 |                              |                           |
 |                              |<-- Approve / Deny -------|
 |                              |                           |
 |-- GET /api/v1/approvals/{id} (poll) -------------------->|
 |<-- { status: "approved" } --|                           |
 |                              |                           |
 |-- Execute callback --------->|                           |

Architecture

Runplane uses a Gateway-first architecture:

  • POST /api/v1/guard is the primary decision endpoint
  • The SDK is optional
  • The Gateway is the control point
  • All decisions are made before execution

If you prefer direct API calls instead of the SDK:

curl -X POST https://your-instance.com/api/v1/guard \
  -H "Authorization: Bearer $RUNPLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": {
      "type": "delete_record",
      "target": "users-database",
      "context": { "recordId": "user_123" }
    }
  }'

Error handling

All SDK errors are thrown as ShieldError and include a code.

Code Meaning
BLOCKED Action was blocked by policy, or execution was blocked because fail-closed was triggered
DENIED Human approver denied the action
TIMEOUT Approval polling timed out
NETWORK_ERROR Network or API failure
UNKNOWN Unexpected error
import { Shield, ShieldError } from "@runplane/runplane-sdk";

try {
  await runplane.guard("action", "target", {}, async () => {
    // ...
  });
} catch (err) {
  if (err instanceof ShieldError) {
    switch (err.code) {
      case "BLOCKED":
        console.log("Action blocked:", err.message);
        break;
      case "DENIED":
        console.log("Approver denied:", err.message);
        break;
      case "TIMEOUT":
        console.log("Approval timed out");
        break;
      case "NETWORK_ERROR":
        console.log("Network or API failure:", err.message);
        break;
      default:
        console.log("Unexpected error:", err.message);
    }
  }
}

Fail-closed behavior

By default, the SDK uses failMode: "closed".

That means failures do not allow the action to proceed automatically.

Examples:

  • Invalid API key can result in blocked execution
  • Network failures can result in blocked execution
  • Missing approvalId throws an error
  • Approval timeout can result in blocked execution, depending on the flow

Use failMode: "open" only if you explicitly want the action to continue on failure.

Troubleshooting

SyntaxError: Cannot use import statement outside a module

Your project is likely running in CommonJS mode.

Fix one of these:

  • Set "type": "module" in package.json
  • Rename the file to .mjs
  • Use the CommonJS example with require

invalid_key

Your API key is missing, malformed, expired, or belongs to a different environment/account.

Checks:

  • Verify the environment variable is set
  • Verify you copied the full key
  • Regenerate the key if needed
  • Never paste production keys into public chats or logs

Script appears stuck

Most likely the action is waiting for human approval.

Checks:

  • Open the Runplane dashboard
  • Go to Approvals
  • Approve or deny the pending request
  • For local testing, temporarily reduce approvalTimeoutMs

API reference

guard<T>(actionType, target, context, fn): Promise<T>

Guard an action before execution.

Parameter Type Description
actionType string Canonical action name, for example "delete_record"
target string Target system identifier
context Record<string, unknown> | null Additional context for policy evaluation
fn () => Promise<T> Callback to execute if allowed

decide(req): Promise<DecideResponse>

Request a decision without executing a callback.

const response = await runplane.decide({
  actionType: "transfer_funds",
  target: "banking-api",
  context: { amount: 50000 },
});

if (response.decision === "ALLOW") {
  // proceed with action
}

pollApproval(approvalId): Promise<ApprovalPollResponse>

Poll for approval status. Used internally by guard(), but available for custom flows.

What changed in v1.2.0

  • The SDK now uses /api/v1/guard instead of /api/decide
  • Approval polling now uses approvalId
  • Error messages for timeout and denial were improved
  • The SDK is now explicitly positioned as a wrapper over the Gateway

Compatibility

The package is backward compatible. Existing guard() integrations continue to work without signature changes.

Deprecation notice

The legacy /api/decide endpoint is deprecated but remains available for backward compatibility. New integrations should use /api/v1/guard directly or through this SDK.

License

MIT