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\"}"
}

Inputs

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

Outputs

After the client resumes the Flow, the step output variable contains the client-provided result. Successful tool calls store toolResult.output; failed calls store an object with error, message, and executionId.

The step definition exposes these output fields to the editor:

  • result: Tool result returned by the client.
  • error: Whether the client reported a failed execution.
  • executionId: Unique ID generated for the pending tool call.

The Flow step's normal output field controls the context variable name used by downstream steps.

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, for example variable_name inside an expression block
  • Check that previous steps output the required variables
  • Use str(variable) inside an expression block for complex objects

See Also

Built with VitePress