Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.webrun.ai/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Guardrails are safety mechanisms that pause agent execution when:
  • The agent needs sensitive information (credentials, payment details)
  • The agent is uncertain and requires human guidance
  • The agent encounters content that requires human verification
  • The agent detects potential policy violations
When a guardrail triggers, the task pauses and waits for your response before continuing.
Prefer secrets for login credentials. If you know which sites the agent will authenticate with, use the secrets parameter to provide credentials upfront. This avoids guardrail interruptions entirely, so the task runs without pausing. Secrets are never stored — they are discarded when the session ends.

Detecting Guardrails

Guardrails are detected differently depending on your integration method.

REST API (Polling)

When polling a task endpoint, a guardrail appears as:
curl https://connect.webrun.ai/task/SESSION_ID/TASK_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
Response:
{
  "success": true,
  "type": "guardrail_trigger",
  "data": {
    "type": "human_input_needed",
    "value": "I need login credentials to proceed"
  }
}

WebSocket (Real-Time)

With WebSocket connections, you receive immediate notifications:
socket.on("message", (data) => {
  if (data.type === "guardrail_trigger") {
    console.log("Guardrail triggered:", data.data.type);
    console.log("Agent says:", data.data.value);

    // Handle guardrail response
    handleGuardrail(data.data);
  }
});
Example guardrail event:
{
  "type": "guardrail_trigger",
  "data": {
    "type": "human_input_needed",
    "value": "I need the login credentials to continue"
  }
}

Responding to Guardrails

Once a guardrail triggers, respond with the requested information or guidance. The newState: "resume" parameter tells the agent to continue execution from the exact point where the guardrail paused—it doesn’t restart the task.
curl -X POST https://connect.webrun.ai/start/send-message \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "sessionId": "SESSION_ID",
    "message": {
      "actionType": "guardrail",
      "prompt": "Username: demo@example.com, Password: demo123",
      "newState": "resume"
    }
  }'
ParameterTypeRequiredDescription
actionTypestringYesMust be "guardrail"
promptstringYesYour response to the agent
newStatestringYes"resume" to continue, "stop" to cancel
Response:
{
  "success": true,
  "message": "Message sent successfully"
}

Routing Guardrails to a Chat User

Polling and WebSocket both assume something on your side is watching the session when a guardrail triggers. For unattended workloads — scheduled triggers, long-running CI jobs, MCP tasks kicked off from another tool — that isn’t always the case. As a fallback, a guardrail can be delivered to a chat user connected to the session’s environment, and their reply is forwarded back to the session.

How it works

When a session raises a guardrail, the platform first checks whether you’re actively handling it (polling responses or a live WebSocket subscription on the session). If you are, the prompt reaches you the usual way and nothing else happens. If no one is actively watching, the platform looks for a chat user (Telegram, WhatsApp, Slack, Discord, or Teams) connected to the same environment. If one is available, the guardrail prompt is sent to them in chat. Their reply is forwarded to the session exactly like a regular guardrail response — resume to continue or stop to cancel. Whoever answers first wins. If your API caller and a chat user both respond at the same time, only the first reply reaches the session.

Choosing a reach-out mode

Chat reach-out is controlled per session by the reachOutMode parameter. It takes one of three values, and defaults to "guardrail_only":
ValueBehavior
"off"No proactive chat messages. Guardrails and results stay on the API/WebSocket channel only.
"guardrail_only"Default. The bot pings the chat user only when the session hits a guardrail (CAPTCHA, 2FA, verification, login, etc.).
"full"The bot pings on guardrails and also delivers the task result to chat when the task completes.
To turn chat routing off for a specific session, pass "reachOutMode": "off" at session creation:
{
  "environmentId": "683a1f2e4b0c1d2e3f4a5b6c",
  "reachOutMode": "off",
  "mode": "default",
  "task": {
    "prompt": "Export the monthly financial report and download the PDF"
  }
}
reachOutMode is set at session creation and applies for the lifetime of the session. There is no way to change it later — start a new session if you need different behavior.

When to set reachOutMode to "off"

  • Sensitive sessions. Anything involving private credentials, financial data, or personal information you don’t want surfaced through chat.
  • Shared environments. When multiple people use the same environment but only your code should handle guardrails for this session.
  • Deterministic flows. Automations that must respond programmatically — turning chat routing off ensures the prompt always waits for your API call instead of being answered by someone else first.

When to use "full"

Use "full" for unattended automations where a human on chat should also receive the task result — for example, a scheduled run kicked off via MCP or REST with no live client polling. When the session completes, the chat user is sent the task result in addition to (not instead of) the normal API/webhook delivery. The chat fan-out only fires when the caller is offline (no live WebSocket subscriber on the session).
If you’d rather avoid guardrails entirely for known auth flows, provide credentials upfront with secrets instead. Secrets are never stored and are scoped to the session.

Common Guardrail Scenarios

1. Login Credentials

Guardrail:
{
  "type": "guardrail_trigger",
  "data": {
    "type": "human_input_needed",
    "value": "I need login credentials for this site"
  }
}
Response:
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Use username: user@example.com and password: mypassword123",
  newState: "resume"
});
Alternative (manual input):
// Take control and enter credentials manually
socket.emit("message", {
  actionType: "interaction",
  action: { type: "takeOverControl" }
});

// Enter credentials via manual interaction
socket.emit("message", {
  actionType: "interaction",
  action: { type: "CLICK", x: 400, y: 300 }
});

socket.emit("message", {
  actionType: "interaction",
  action: { type: "TYPE", text: "user@example.com", humanLike: true }
});

// Release control
socket.emit("message", {
  actionType: "interaction",
  action: { type: "releaseControl" }
});

// Resume task
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Credentials entered, please continue",
  newState: "resume"
});

2. Payment Information

Guardrail:
{
  "type": "guardrail_trigger",
  "data": {
    "type": "human_input_needed",
    "value": "Payment information required to complete checkout"
  }
}
Response (provide details):
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Card number: 4111111111111111, Expiry: 12/25, CVV: 123",
  newState: "resume"
});
Response (cancel):
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Do not proceed with payment",
  newState: "stop"
});

3. Ambiguous Instructions

Guardrail:
{
  "type": "guardrail_trigger",
  "data": {
    "type": "human_input_needed",
    "value": "I found 5 products matching 'wireless keyboard'. Which one should I select?"
  }
}
Response:
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Select the first one with the highest rating",
  newState: "resume"
});

4. Verification Needed

Guardrail:
{
  "type": "guardrail_trigger",
  "data": {
    "type": "human_input_needed",
    "value": "This action requires two-factor authentication code"
  }
}
Response:
// Get 2FA code from your system
const twoFactorCode = await getTwoFactorCode();

socket.emit("message", {
  actionType: "guardrail",
  prompt: `2FA code: ${twoFactorCode}`,
  newState: "resume"
});

5. CAPTCHA Detection

Guardrail:
{
  "type": "guardrail_trigger",
  "data": {
    "type": "human_input_needed",
    "value": "CAPTCHA detected, manual solving required"
  }
}
Response (manual solving):
// Take control for manual CAPTCHA solving
socket.emit("message", {
  actionType: "interaction",
  action: { type: "takeOverControl" }
});

// User solves CAPTCHA via video stream...
// Once solved:

socket.emit("message", {
  actionType: "interaction",
  action: { type: "releaseControl" }
});

socket.emit("message", {
  actionType: "guardrail",
  prompt: "CAPTCHA solved, continue",
  newState: "resume"
});

6. Content Verification

Guardrail:
{
  "type": "guardrail_trigger",
  "data": {
    "type": "human_input_needed",
    "value": "I found content that may be sensitive. Please verify before proceeding."
  }
}
Response (approve):
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Content verified, safe to proceed",
  newState: "resume"
});
Response (reject):
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Do not proceed with this content",
  newState: "stop"
});

Automated Guardrail Handling

For predictable guardrails (like login credentials), implement automated handling:

Pattern 1: Credential Manager

class GuardrailHandler {
  constructor(credentials) {
    this.credentials = credentials;
  }

  handle(guardrailData) {
    const message = guardrailData.value.toLowerCase();

    // Detect login request
    if (message.includes("login") || message.includes("credentials")) {
      return {
        actionType: "guardrail",
        prompt: `Username: ${this.credentials.username}, Password: ${this.credentials.password}`,
        newState: "resume"
      };
    }

    // Detect 2FA request
    if (message.includes("2fa") || message.includes("two-factor")) {
      const code = this.getTwoFactorCode();
      return {
        actionType: "guardrail",
        prompt: `2FA code: ${code}`,
        newState: "resume"
      };
    }

    // Detect payment request
    if (message.includes("payment") || message.includes("credit card")) {
      return {
        actionType: "guardrail",
        prompt: "Do not proceed with payment",
        newState: "stop"
      };
    }

    // Default: require human intervention
    return null;
  }

  getTwoFactorCode() {
    // Integrate with your 2FA system
    return "123456";
  }
}

// Usage
const handler = new GuardrailHandler({
  username: "user@example.com",
  password: "securepassword123"
});

socket.on("message", (data) => {
  if (data.type === "guardrail_trigger") {
    const response = handler.handle(data.data);

    if (response) {
      // Automated response
      socket.emit("message", response);
    } else {
      // Escalate to human
      console.log("Human intervention required:", data.data.value);
      notifyHuman(data.data);
    }
  }
});

Pattern 2: Rule-Based Handler

const guardrailRules = [
  {
    pattern: /login|credentials|username|password/i,
    response: (data) => ({
      actionType: "guardrail",
      prompt: process.env.LOGIN_CREDENTIALS,
      newState: "resume"
    })
  },
  {
    pattern: /captcha/i,
    response: (data) => {
      // Trigger manual intervention
      return "MANUAL";
    }
  },
  {
    pattern: /payment|credit card|billing/i,
    response: (data) => ({
      actionType: "guardrail",
      prompt: "Skip payment step",
      newState: "stop"
    })
  },
  {
    pattern: /which.*select|choose.*option/i,
    response: (data) => ({
      actionType: "guardrail",
      prompt: "Select the first option",
      newState: "resume"
    })
  }
];

function handleGuardrailWithRules(guardrailData) {
  const message = guardrailData.value;

  for (const rule of guardrailRules) {
    if (rule.pattern.test(message)) {
      const response = rule.response(guardrailData);

      if (response === "MANUAL") {
        console.log("Manual intervention required");
        return null;
      }

      return response;
    }
  }

  // No rule matched, require human
  return null;
}

// Usage
socket.on("message", (data) => {
  if (data.type === "guardrail_trigger") {
    const response = handleGuardrailWithRules(data.data);

    if (response) {
      socket.emit("message", response);
    } else {
      // Escalate to human
      alertHuman(data.data);
    }
  }
});

Pattern 3: Async Handler with Timeout

class AsyncGuardrailHandler {
  constructor(timeout = 30000) {
    this.timeout = timeout;
    this.pendingGuardrails = new Map();
  }

  async handle(sessionId, guardrailData) {
    // Try automated handling first
    const autoResponse = this.tryAutomatic(guardrailData);

    if (autoResponse) {
      return autoResponse;
    }

    // Fall back to human with timeout
    return this.requestHumanInput(sessionId, guardrailData);
  }

  tryAutomatic(guardrailData) {
    const message = guardrailData.value.toLowerCase();

    if (message.includes("login")) {
      return {
        actionType: "guardrail",
        prompt: `Username: ${process.env.USERNAME}, Password: ${process.env.PASSWORD}`,
        newState: "resume"
      };
    }

    return null;
  }

  async requestHumanInput(sessionId, guardrailData) {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        this.pendingGuardrails.delete(sessionId);
        reject(new Error("Guardrail response timeout"));
      }, this.timeout);

      this.pendingGuardrails.set(sessionId, {
        resolve: (response) => {
          clearTimeout(timeoutId);
          this.pendingGuardrails.delete(sessionId);
          resolve(response);
        },
        reject,
        data: guardrailData
      });

      // Notify UI/human
      this.notifyHuman(sessionId, guardrailData);
    });
  }

  respondToGuardrail(sessionId, prompt, shouldResume = true) {
    const pending = this.pendingGuardrails.get(sessionId);

    if (pending) {
      pending.resolve({
        actionType: "guardrail",
        prompt,
        newState: shouldResume ? "resume" : "stop"
      });
    }
  }

  notifyHuman(sessionId, data) {
    console.log(`[${sessionId}] Human input needed:`, data.value);
    // Send notification to UI, Slack, email, etc.
  }
}

// Usage
const handler = new AsyncGuardrailHandler(30000);

socket.on("message", async (data) => {
  if (data.type === "guardrail_trigger") {
    try {
      const response = await handler.handle(session.sessionId, data.data);
      socket.emit("message", response);
    } catch (error) {
      console.error("Guardrail handling failed:", error);
      // Stop the task
      socket.emit("message", {
        actionType: "state",
        newState: "stop"
      });
    }
  }
});

// Human responds via UI
app.post("/api/respond-guardrail", (req, res) => {
  const { sessionId, response, shouldResume } = req.body;
  handler.respondToGuardrail(sessionId, response, shouldResume);
  res.json({ success: true });
});

Best Practices

1. Never Hardcode Sensitive Data

Don’t put credentials directly in code:
// Bad
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Username: admin, Password: admin123",
  newState: "resume"
});

// Good
socket.emit("message", {
  actionType: "guardrail",
  prompt: `Username: ${process.env.USERNAME}, Password: ${process.env.PASSWORD}`,
  newState: "resume"
});

2. Implement Timeouts

Always set timeouts for human intervention:
const GUARDRAIL_TIMEOUT = 60000; // 1 minute

const timeoutPromise = new Promise((_, reject) =>
  setTimeout(() => reject(new Error("Timeout")), GUARDRAIL_TIMEOUT)
);

const responsePromise = waitForHumanResponse(sessionId);

try {
  const response = await Promise.race([responsePromise, timeoutPromise]);
  socket.emit("message", response);
} catch (error) {
  console.log("Timeout - stopping task");
  socket.emit("message", { actionType: "state", newState: "stop" });
}

3. Log All Guardrails

Track guardrail occurrences for debugging and improvement:
function logGuardrail(sessionId, guardrailData, response) {
  console.log({
    timestamp: new Date().toISOString(),
    sessionId,
    guardrailType: guardrailData.type,
    guardrailMessage: guardrailData.value,
    responseType: response ? "automated" : "manual",
    response: response?.prompt
  });

  // Send to logging service
  analytics.track("guardrail_triggered", {
    sessionId,
    type: guardrailData.type,
    automated: !!response
  });
}

4. Provide Clear Responses

Be specific in your guardrail responses:
// Bad - vague
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Yes, do it",
  newState: "resume"
});

// Good - specific
socket.emit("message", {
  actionType: "guardrail",
  prompt: "Select the product titled 'Logitech K380' from the search results",
  newState: "resume"
});