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-sdkQuick start
Option A: ESM (recommended)
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.jsWindows CMD
set RUNPLANE_API_KEY=your_api_key_here
node index.jsWindows PowerShell
$env:RUNPLANE_API_KEY="your_api_key_here"
node index.jsWhat 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:
- The SDK sends the action to the Guard API.
- The API returns
REQUIRE_APPROVALand anapprovalId. - The SDK polls the approval endpoint until the request is resolved.
- If approved, the callback executes.
- If denied, the SDK throws
ShieldErrorwith code"DENIED". - 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/guardis 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
approvalIdthrows 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"inpackage.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/guardinstead 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