Skip to main content

Documentation Index

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

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

Execute agent tools in the browser via WebSocket. Handle real-time tool requests from the agent and return results — for UI interactions, form manipulation, and client-side state access. What you’ll learn: WebSocket vs webhook tools, tool configuration, request handling, response format, and error handling.
Testing requires a valid API key. Generate a web integration token from your agent’s Web Integrations settings in the Dasha BlackBox dashboard before starting.

When to use WebSocket tools

WebSocket tool execution is designed for tools that need direct access to the user’s browser or application state: Use WebSocket tools for:
  • UI interactions (clicking buttons, navigating tabs, scrolling)
  • Form manipulation (filling inputs, selecting options)
  • Browser actions (opening windows, navigating URLs)
  • Client-side state (localStorage, sessionStorage, DOM access)
  • Real-time user feedback (animations, notifications)
Use webhook tools for:
  • Server-side API calls
  • Database queries
  • Email sending
  • Payment processing
  • Any operation requiring server-side secrets
WebSocket tools execute in the browser. Never use them for operations requiring server-side secrets or authentication credentials.

Step 1: Configure tools for WebSocket

Tools must be explicitly configured for WebSocket execution in your web integration.

Via dashboard

  1. Go to Dashboard → Agents → [Your Agent] → Web Integrations
  2. Select your integration
  3. Navigate to Features tab
  4. Under Tools, add tool names to execute via WebSocket
Include tool names in your web integration’s tools array:
curl -X PATCH https://blackbox.dasha.ai/api/v1/webIntegrations/{integrationId} \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tools": ["scrollToElement", "fillFormField", "clickButton"]
  }'
Listen for websocketToolRequest messages and respond with websocketToolResponse:
const ws = new WebSocket(
  `wss://blackbox.dasha.ai/api/v1/ws/webCall?token=${token}`
);

// Tool handler registry
const toolHandlers = {
  scrollToElement: async ({ selector }) => {
    const element = document.querySelector(selector);
    if (!element) {
      return { success: false, error: 'Element not found' };
    }
    element.scrollIntoView({ behavior: 'smooth' });
    return { success: true };
  },

  fillFormField: async ({ selector, value }) => {
    const element = document.querySelector(selector);
    if (!element) {
      return { success: false, error: 'Element not found' };
    }
    element.value = value;
    element.dispatchEvent(new Event('input', { bubbles: true }));
    return { success: true, value };
  },

  clickButton: async ({ selector }) => {
    const element = document.querySelector(selector);
    if (!element) {
      return { success: false, error: 'Element not found' };
    }
    element.click();
    return { success: true };
  }
};

ws.onmessage = async (event) => {
  const message = JSON.parse(event.data);

  switch (message.type) {
    case 'websocketToolRequest':
      await handleToolRequest(message);
      break;

    case 'text':
      if (message.content.source === 'assistant') {
        displayMessage(message.content.text);
      }
      break;

    case 'toolCall':
      console.log(`Agent calling tool: ${message.name}`);
      showToolIndicator(message.name);
      break;

    case 'toolCallResult':
      console.log(`Tool completed: ${message.name}`);
      hideToolIndicator(message.name);
      break;
  }
};

async function handleToolRequest(message) {
  const { id, toolName, args } = message.content;

  try {
    const handler = toolHandlers[toolName];
    if (!handler) {
      throw new Error(`Unknown tool: ${toolName}`);
    }

    const result = await handler(args);

    ws.send(JSON.stringify({
      type: 'websocketToolResponse',
      timestamp: new Date().toISOString(),
      content: { id, result }
    }));
  } catch (error) {
    ws.send(JSON.stringify({
      type: 'websocketToolResponse',
      timestamp: new Date().toISOString(),
      content: {
        id,
        result: {
          success: false,
          error: error.message
        }
      }
    }));
  }
}
{
  "type": "websocketToolRequest",
  "timestamp": "2025-01-20T10:02:00Z",
  "channelId": null,
  "content": {
    "id": "wtr-abc123",
    "toolName": "fillFormField",
    "args": {
      "selector": "#email-input",
      "value": "user@example.com"
    }
  }
}
{
  "type": "websocketToolResponse",
  "timestamp": "2025-01-20T10:02:01Z",
  "content": {
    "id": "wtr-abc123",
    "result": {
      "success": true,
      "value": "user@example.com"
    }
  }
}

Step 3: Tool implementation examples

const formTools = {
  // Fill any form field by selector
  setFieldValue: async ({ selector, value }) => {
    const element = document.querySelector(selector);
    if (!element) {
      return { success: false, error: `Element not found: ${selector}` };
    }

    // Handle different input types
    if (element.tagName === 'SELECT') {
      element.value = value;
      element.dispatchEvent(new Event('change', { bubbles: true }));
    } else if (element.type === 'checkbox' || element.type === 'radio') {
      element.checked = Boolean(value);
      element.dispatchEvent(new Event('change', { bubbles: true }));
    } else {
      element.value = value;
      element.dispatchEvent(new Event('input', { bubbles: true }));
    }

    return { success: true, selector, value };
  },

  // Get current form values
  getFormData: async ({ formSelector }) => {
    const form = document.querySelector(formSelector);
    if (!form) {
      return { success: false, error: 'Form not found' };
    }

    const formData = new FormData(form);
    const data = Object.fromEntries(formData.entries());

    return { success: true, data };
  },

  // Submit a form
  submitForm: async ({ formSelector }) => {
    const form = document.querySelector(formSelector);
    if (!form) {
      return { success: false, error: 'Form not found' };
    }

    form.submit();
    return { success: true };
  }
};
const dataTools = {
  // Read displayed content
  getElementText: async ({ selector }) => {
    const element = document.querySelector(selector);
    if (!element) {
      return { success: false, error: 'Element not found' };
    }

    return {
      success: true,
      text: element.textContent?.trim() || '',
      html: element.innerHTML
    };
  },

  // Get cart items (e-commerce example)
  getCartItems: async () => {
    const items = Array.from(document.querySelectorAll('.cart-item')).map(item => ({
      name: item.querySelector('.item-name')?.textContent,
      price: item.querySelector('.item-price')?.textContent,
      quantity: item.querySelector('.item-quantity')?.value
    }));

    const total = document.querySelector('.cart-total')?.textContent;

    return {
      success: true,
      items,
      total,
      itemCount: items.length
    };
  },

  // Get page state
  getPageState: async () => {
    return {
      success: true,
      url: window.location.href,
      title: document.title,
      scrollPosition: {
        x: window.scrollX,
        y: window.scrollY
      }
    };
  }
};

Step 4: Complete implementation

import { useState, useEffect, useRef, useCallback } from 'react';

interface ToolExecution {
  id: string;
  name: string;
  status: 'pending' | 'executing' | 'completed' | 'error';
  args: unknown;
  result?: unknown;
}

interface ChatWithToolsProps {
  token: string;
  tools: Record<string, (args: unknown) => Promise<unknown>>;
}

export function ChatWithTools({ token, tools }: ChatWithToolsProps) {
  const [messages, setMessages] = useState<Array<{
    sender: 'user' | 'agent';
    text: string;
  }>>([]);
  const [executions, setExecutions] = useState<ToolExecution[]>([]);
  const [input, setInput] = useState('');
  const wsRef = useRef<WebSocket | null>(null);

  const handleToolRequest = useCallback(async (
    id: string,
    toolName: string,
    args: unknown
  ) => {
    // Update execution status
    setExecutions(prev => [
      ...prev,
      { id, name: toolName, status: 'executing', args }
    ]);

    try {
      const handler = tools[toolName];
      if (!handler) {
        throw new Error(`Unknown tool: ${toolName}`);
      }

      const result = await handler(args);

      setExecutions(prev => prev.map(e =>
        e.id === id ? { ...e, status: 'completed', result } : e
      ));

      wsRef.current?.send(JSON.stringify({
        type: 'websocketToolResponse',
        timestamp: new Date().toISOString(),
        content: { id, result }
      }));
    } catch (error) {
      const errorResult = {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error'
      };

      setExecutions(prev => prev.map(e =>
        e.id === id ? { ...e, status: 'error', result: errorResult } : e
      ));

      wsRef.current?.send(JSON.stringify({
        type: 'websocketToolResponse',
        timestamp: new Date().toISOString(),
        content: { id, result: errorResult }
      }));
    }
  }, [tools]);

  useEffect(() => {
    const ws = new WebSocket(
      `wss://blackbox.dasha.ai/api/v1/ws/webCall?token=${token}`
    );
    wsRef.current = ws;

    ws.onopen = () => {
      ws.send(JSON.stringify({
        type: 'initialize',
        timestamp: new Date().toISOString(),
        request: { callType: 'chat', additionalData: {} }
      }));
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);

      switch (message.type) {
        case 'text':
          if (message.content.source === 'assistant' && message.content.text) {
            setMessages(prev => [...prev, {
              sender: 'agent',
              text: message.content.text
            }]);
          }
          break;

        case 'websocketToolRequest':
          handleToolRequest(
            message.content.id,
            message.content.toolName,
            message.content.args
          );
          break;

        case 'toolCall':
          setExecutions(prev => [
            ...prev,
            { id: message.callId, name: message.name, status: 'pending', args: message.args }
          ]);
          break;
      }
    };

    return () => ws.close();
  }, [token, handleToolRequest]);

  const sendMessage = () => {
    const text = input.trim();
    if (!text || !wsRef.current) return;

    setMessages(prev => [...prev, { sender: 'user', text }]);
    wsRef.current.send(JSON.stringify({
      type: 'incomingChatMessage',
      content: text,
      timestamp: new Date().toISOString()
    }));
    setInput('');
  };

  return (
    <div className="chat-with-tools">
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={`message ${msg.sender}`}>
            {msg.text}
          </div>
        ))}
      </div>

      {executions.length > 0 && (
        <div className="tool-executions">
          <h4>Tool Executions</h4>
          {executions.map(exec => (
            <div key={exec.id} className={`execution ${exec.status}`}>
              <span className="tool-name">{exec.name}</span>
              <span className="tool-status">{exec.status}</span>
            </div>
          ))}
        </div>
      )}

      <div className="input-area">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Type a message..."
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  );
}

// Usage example
const myTools = {
  highlightElement: async ({ selector }: { selector: string }) => {
    const el = document.querySelector(selector);
    if (!el) return { success: false, error: 'Not found' };
    (el as HTMLElement).style.outline = '3px solid yellow';
    return { success: true };
  },

  showNotification: async ({ message }: { message: string }) => {
    alert(message);
    return { success: true };
  }
};

// <ChatWithTools token="YOUR_TOKEN" tools={myTools} />
Always return structured error responses:
async function handleToolRequest(message) {
  const { id, toolName, args } = message.content;

  try {
    // Validate tool exists
    const handler = toolHandlers[toolName];
    if (!handler) {
      throw new ToolError('UNKNOWN_TOOL', `Unknown tool: ${toolName}`);
    }

    // Validate arguments
    const validation = validateArgs(toolName, args);
    if (!validation.valid) {
      throw new ToolError('INVALID_ARGS', validation.errors.join(', '));
    }

    // Execute with timeout
    const result = await Promise.race([
      handler(args),
      new Promise((_, reject) =>
        setTimeout(() => reject(new ToolError('TIMEOUT', 'Tool execution timeout')), 30000)
      )
    ]);

    ws.send(JSON.stringify({
      type: 'websocketToolResponse',
      timestamp: new Date().toISOString(),
      content: { id, result }
    }));

  } catch (error) {
    ws.send(JSON.stringify({
      type: 'websocketToolResponse',
      timestamp: new Date().toISOString(),
      content: {
        id,
        result: {
          success: false,
          error: error.message,
          code: error.code || 'UNKNOWN_ERROR'
        }
      }
    }));
  }
}

class ToolError extends Error {
  constructor(code, message) {
    super(message);
    this.code = code;
  }
}

Best practices

function validateArgs(toolName, args) {
  const schemas = {
    fillFormField: { selector: 'string', value: 'string' },
    scrollToElement: { selector: 'string' },
    navigateTo: { url: 'string' }
  };

  const schema = schemas[toolName];
  if (!schema) return { valid: true };

  const errors = [];
  for (const [key, type] of Object.entries(schema)) {
    if (typeof args[key] !== type) {
      errors.push(`${key} must be a ${type}`);
    }
  }

  return { valid: errors.length === 0, errors };
}
async function handleToolRequest(message) {
  const { id, toolName, args } = message.content;
  const startTime = performance.now();

  console.log(`[Tool] ${toolName} started`, { id, args });

  try {
    const result = await toolHandlers[toolName](args);
    const duration = Math.round(performance.now() - startTime);
    console.log(`[Tool] ${toolName} completed in ${duration}ms`, { result });

    // Send response...
  } catch (error) {
    const duration = Math.round(performance.now() - startTime);
    console.error(`[Tool] ${toolName} failed after ${duration}ms`, { error });

    // Send error response...
  }
}
const asyncTools = {
  // Wait for element to appear
  waitForElement: async ({ selector, timeout = 5000 }) => {
    const startTime = Date.now();

    while (Date.now() - startTime < timeout) {
      const element = document.querySelector(selector);
      if (element) {
        return { success: true, found: true };
      }
      await new Promise(resolve => setTimeout(resolve, 100));
    }

    return { success: false, error: 'Element not found within timeout' };
  },

  // Retry operation
  retryOperation: async ({ toolName, args, maxRetries = 3 }) => {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        const result = await toolHandlers[toolName](args);
        return { ...result, attempts: attempt };
      } catch (error) {
        if (attempt === maxRetries) {
          return { success: false, error: error.message, attempts: attempt };
        }
        await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
      }
    }
  }
};

Troubleshooting

Tool not being called

Possible causes:
  • Tool not configured in web integration
  • Tool name mismatch between agent config and handler
  • Agent doesn’t understand when to use the tool
Solution: Verify tool name in dashboard matches exactly, check agent’s tool descriptions are clear.
Possible causes:
  • Handler takes too long to execute
  • Handler never returns
  • DOM operation fails silently
Solution: Add timeouts, logging, and error boundaries:
const withTimeout = (fn, ms = 10000) => async (...args) => {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([fn(...args), timeout]);
};

const safeTools = Object.fromEntries(
  Object.entries(tools).map(([name, fn]) => [name, withTimeout(fn)])
);
Possible causes:
  • Result format doesn’t match what agent expects
  • Error in result not clearly communicated
  • Agent prompt doesn’t instruct how to use results
Solution: Use consistent result structure, include descriptive messages:
// Good: Clear, structured result
return {
  success: true,
  data: { orderId: 'ORD-123', status: 'shipped' },
  message: 'Order found and is currently shipped'
};

// Bad: Ambiguous result
return { orderId: 'ORD-123' };

Next steps

Message Reference

Complete WebSocket message documentation

Error Handling

Handle errors and edge cases