Skip to content

Client Tool Step

The Client Tool step enables flows to pause execution and request the client (browser or application) to execute a JavaScript function. This creates a powerful bridge between server-side flow automation and client-side operations, allowing flows to access local data, APIs, or perform operations that need to run on the client.

Overview

The Client Tool step:

  • Pauses flow execution at a specific point
  • Requests the client to execute a named tool with parameters
  • Waits for the client to provide the execution result
  • Resumes flow execution with the tool result
  • Enables seamless integration between server flows and client-side logic

Use Cases

Common Scenarios

  • Access Browser Data: Read localStorage, sessionStorage, or cookies
  • External API Calls: Make API calls from the client to avoid CORS issues
  • Database Queries: Query local databases (IndexedDB, SQLite)
  • User Interactions: Show dialogs, get user input, or display notifications
  • Device Access: Access camera, microphone, or geolocation
  • Custom Business Logic: Execute proprietary calculations or validations
  • Third-Party Integrations: Integrate with client-side SDKs (Stripe, Analytics, etc.)

Configuration

Basic Setup

json
{
  "id": "check-inventory",
  "type": "client-tool",
  "name": "Check Product Inventory",
  "data": {
    "toolName": "getInventoryStatus",
    "parameters": [
      { "key": "productId", "value": "{{product_id}}" },
      { "key": "warehouseCode", "value": "{{warehouse}}" }
    ]
  },
  "nextStepId": "process-result",
  "output": "inventory_data"
}

Parameters Format Options

The Client Tool step supports three parameter formats:

json
{
  "parameters": [
    { "key": "location", "value": "{{city}}" },
    { "key": "unit", "value": "celsius" }
  ]
}

2. Object Format

json
{
  "parameters": {
    "location": "{{city}}",
    "unit": "celsius"
  }
}

3. JSON String Format

json
{
  "parameters": "{\"location\": \"{{city}}\", \"unit\": \"celsius\"}"
}

Step Properties

PropertyTypeRequiredDescription
toolNamestringYesName of the client-side tool to execute
parametersarray/object/stringNoParameters to pass to the tool (supports template variables)
outputstringYesVariable name to store the tool result

Template Variables

All parameter values support template variable substitution:

json
{
  "toolName": "getUserOrders",
  "parameters": [
    { "key": "userId", "value": "{{user.id}}" },
    { "key": "startDate", "value": "{{filters.startDate}}" },
    { "key": "limit", "value": "{{request_limit}}" }
  ]
}

Client-Side Implementation

Using Cognipeer SDK

When using the Cognipeer SDK, client tools are automatically handled:

typescript
import { CognipeerClient } from '@cognipeer/sdk';

const client = new CognipeerClient({ token: 'your-token' });

// Execute flow with client tools
const result = await client.flows.execute('flow-id', {
  inputs: { productId: '12345' }
});

// SDK automatically pauses when client-tool step is encountered
// and executes the registered tool implementation

Registering Client Tools

Define client tool implementations when executing flows:

typescript
const result = await client.flows.execute('flow-id', {
  inputs: { productId: '12345' },
  clientTools: [{
    type: 'function',
    function: {
      name: 'getInventoryStatus',
      description: 'Check product inventory levels',
      parameters: {
        type: 'object',
        properties: {
          productId: { type: 'string' },
          warehouseCode: { type: 'string' }
        },
        required: ['productId']
      }
    },
    implementation: async ({ productId, warehouseCode }) => {
      // Query local database or API
      const inventory = await inventoryDB.query({
        productId,
        warehouse: warehouseCode
      });
      
      return {
        inStock: inventory.quantity > 0,
        quantity: inventory.quantity,
        warehouse: warehouseCode,
        lastUpdated: inventory.updatedAt
      };
    }
  }]
});

Flow Execution Lifecycle

Step-by-Step Process

  1. Flow Reaches Client Tool Step

    • Flow execution pauses
    • Server saves current execution state
    • Client receives tool execution request
  2. Client Executes Tool

    • Client SDK finds matching tool by name
    • Tool implementation executes with provided parameters
    • Result is captured
  3. Client Resumes Flow

    • Client sends tool result back to server
    • Server loads saved execution state
    • Flow continues from the next step
  4. Result Available in Context

    • Tool result stored in output variable
    • Subsequent steps can access the result

Visual Flow

┌──────────────────┐
│  Previous Step   │
└────────┬─────────┘


┌──────────────────┐
│  Client Tool     │◄─── Flow Pauses Here
│  (Pause & Wait)  │
└────────┬─────────┘


┌──────────────────┐
│  Client Executes │◄─── Happens on Client
│  Tool Function   │
└────────┬─────────┘


┌──────────────────┐
│  Flow Resumes    │◄─── Result Sent to Server
│  With Result     │
└────────┬─────────┘


┌──────────────────┐
│  Next Step       │◄─── Access via {{tool_result}}
│  (Uses Result)   │
└──────────────────┘

Complete Example

Flow Definition

json
{
  "steps": [
    {
      "id": "form-input",
      "type": "form",
      "trigger": true,
      "data": {
        "inputs": [
          { "name": "user_id", "type": "text" }
        ]
      },
      "nextStepId": "get-user-profile"
    },
    {
      "id": "get-user-profile",
      "type": "client-tool",
      "name": "Fetch User Profile",
      "data": {
        "toolName": "getUserProfile",
        "parameters": [
          { "key": "userId", "value": "{{user_id}}" }
        ]
      },
      "nextStepId": "get-recommendations",
      "output": "user_profile"
    },
    {
      "id": "get-recommendations",
      "type": "client-tool",
      "name": "Get Product Recommendations",
      "data": {
        "toolName": "getRecommendations",
        "parameters": [
          { "key": "userId", "value": "{{user_id}}" },
          { "key": "preferences", "value": "{{user_profile.preferences}}" }
        ]
      },
      "nextStepId": "generate-report",
      "output": "recommendations"
    },
    {
      "id": "generate-report",
      "type": "ask-to-llm",
      "name": "Create Personalized Report",
      "data": {
        "modelId": "chatgpt-4o-mini",
        "messages": [
          {
            "role": "user",
            "content": "Create a personalized recommendation report for user {{user_profile.name}} based on: {{str(recommendations)}}"
          }
        ]
      },
      "nextStepId": "final",
      "output": "report"
    },
    {
      "id": "final",
      "type": "final",
      "data": {
        "outputs": [
          {
            "id": "user-report",
            "name": "Personalized Recommendations",
            "output": "{{report.content}}",
            "type": "markdown"
          }
        ]
      }
    }
  ]
}

Client Implementation

typescript
import { CognipeerClient } from '@cognipeer/sdk';

const client = new CognipeerClient({ token: 'api-token' });

// Execute the flow with client tool implementations
const result = await client.flows.execute('recommendation-flow-id', {
  inputs: {
    user_id: 'user-123'
  },
  clientTools: [
    {
      type: 'function',
      function: {
        name: 'getUserProfile',
        description: 'Get user profile from local database',
        parameters: {
          type: 'object',
          properties: {
            userId: { type: 'string' }
          },
          required: ['userId']
        }
      },
      implementation: async ({ userId }) => {
        const profile = await localDB.users.findOne({ id: userId });
        return {
          name: profile.name,
          email: profile.email,
          preferences: profile.preferences,
          purchaseHistory: profile.purchaseHistory
        };
      }
    },
    {
      type: 'function',
      function: {
        name: 'getRecommendations',
        description: 'Get product recommendations based on user data',
        parameters: {
          type: 'object',
          properties: {
            userId: { type: 'string' },
            preferences: { type: 'object' }
          },
          required: ['userId']
        }
      },
      implementation: async ({ userId, preferences }) => {
        const response = await fetch(`https://api.recommendations.com/suggest`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ userId, preferences })
        });
        return await response.json();
      }
    }
  ]
});

console.log('Flow result:', result);

Error Handling

Tool Not Found

If the client doesn't provide a matching tool implementation:

javascript
// Flow will throw an error
Error: Client tool "getUserProfile" not found or not implemented

Solution: Ensure all tools referenced in the flow are registered:

typescript
await client.flows.execute('flow-id', {
  inputs: {...},
  clientTools: [
    // Register all required tools
    { type: 'function', function: { name: 'getUserProfile', ... }, implementation: ... }
  ]
});

Tool Execution Error

If the tool implementation throws an error:

typescript
implementation: async ({ userId }) => {
  try {
    const data = await fetchData(userId);
    return data;
  } catch (error) {
    // Return error information
    return {
      error: true,
      message: error.message,
      code: error.code
    };
  }
}

Timeout Handling

Flows may timeout if client tools take too long:

typescript
// Configure timeout
const client = new CognipeerClient({ 
  token: 'api-token',
  timeout: 60000  // 60 seconds
});

Best Practices

1. Clear Tool Names

Use descriptive, action-oriented names:

  • getUserProfile, checkInventory, calculateShipping
  • tool1, getData, process

2. Validate Parameters

Always validate parameters in tool implementations:

typescript
implementation: async ({ userId, limit }) => {
  if (!userId) {
    throw new Error('userId is required');
  }
  if (limit && limit > 100) {
    throw new Error('limit cannot exceed 100');
  }
  // ... proceed with logic
}

3. Return Structured Data

Return consistent, well-structured data:

typescript
// ✅ Good
return {
  success: true,
  data: { ... },
  timestamp: new Date().toISOString()
};

// ❌ Avoid
return "Some data";

4. Handle Sensitive Data

Never log or expose sensitive data:

typescript
implementation: async ({ userId, apiKey }) => {
  // Don't log apiKey
  console.log('Fetching data for user:', userId);
  
  const data = await fetch(url, {
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });
  
  // Don't return apiKey
  return { data: data.results };
}

5. Optimize Performance

Cache data when appropriate:

typescript
const cache = new Map();

implementation: async ({ productId }) => {
  if (cache.has(productId)) {
    return cache.get(productId);
  }
  
  const data = await fetchProductData(productId);
  cache.set(productId, data);
  return data;
}

Integration Patterns

Pattern 1: External API Integration

typescript
clientTools: [{
  type: 'function',
  function: {
    name: 'fetchWeatherData',
    parameters: {
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  },
  implementation: async ({ city }) => {
    const response = await fetch(
      `https://api.weather.com/v1/current?city=${city}&key=${API_KEY}`
    );
    return await response.json();
  }
}]

Pattern 2: Local Storage Access

typescript
clientTools: [{
  type: 'function',
  function: {
    name: 'getUserPreferences',
    parameters: { type: 'object', properties: {} }
  },
  implementation: async () => {
    const prefs = localStorage.getItem('user_preferences');
    return prefs ? JSON.parse(prefs) : {};
  }
}]

Pattern 3: Database Query

typescript
clientTools: [{
  type: 'function',
  function: {
    name: 'queryLocalDB',
    parameters: {
      type: 'object',
      properties: {
        table: { type: 'string' },
        filters: { type: 'object' }
      }
    }
  },
  implementation: async ({ table, filters }) => {
    const db = await openDatabase();
    const results = await db.table(table).where(filters).toArray();
    return results;
  }
}]

Accessing Results

After a client tool step executes, access the result using the output variable name:

json
{
  "id": "next-step",
  "type": "ask-to-llm",
  "data": {
    "messages": [
      {
        "role": "user",
        "content": "Analyze this data: {{str(inventory_data)}}"
      }
    ]
  }
}

Available Result Properties

Access nested properties using dot notation:

  • {{ "{{tool_result}}" }} - Full result object
  • {{ "{{tool_result.property}}" }} - Specific property
  • {{ "{{tool_result.nested.value}}" }} - Nested values
  • {{ "{{str(tool_result)}}" }} - Convert to string

Troubleshooting

Issue: Flow Not Pausing

Problem: Flow doesn't pause for client tool execution.

Solution:

  • Ensure step type is client-tool
  • Verify toolName is specified
  • Check that flow is executed via SDK with client tools support

Issue: Tool Not Executing

Problem: Client tool is not being called.

Solution:

  • Verify tool name matches exactly (case-sensitive)
  • Ensure tool is registered in clientTools array
  • Check browser console for errors

Issue: Parameters Not Received

Problem: Tool receives undefined parameters.

Solution:

  • Verify template variables are correct: {{variable_name}}
  • Check that previous steps output the required variables
  • Use {{str(variable)}} for complex objects

See Also

Built with VitePress