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
{
"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:
1. List Format (Recommended for UI)
{
"parameters": [
{ "key": "location", "value": "{{city}}" },
{ "key": "unit", "value": "celsius" }
]
}2. Object Format
{
"parameters": {
"location": "{{city}}",
"unit": "celsius"
}
}3. JSON String Format
{
"parameters": "{\"location\": \"{{city}}\", \"unit\": \"celsius\"}"
}Step Properties
| Property | Type | Required | Description |
|---|---|---|---|
toolName | string | Yes | Name of the client-side tool to execute |
parameters | array/object/string | No | Parameters to pass to the tool (supports template variables) |
output | string | Yes | Variable name to store the tool result |
Template Variables
All parameter values support template variable substitution:
{
"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:
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 implementationRegistering Client Tools
Define client tool implementations when executing flows:
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
Flow Reaches Client Tool Step
- Flow execution pauses
- Server saves current execution state
- Client receives tool execution request
Client Executes Tool
- Client SDK finds matching tool by name
- Tool implementation executes with provided parameters
- Result is captured
Client Resumes Flow
- Client sends tool result back to server
- Server loads saved execution state
- Flow continues from the next step
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
{
"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
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:
// Flow will throw an error
Error: Client tool "getUserProfile" not found or not implementedSolution: Ensure all tools referenced in the flow are registered:
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:
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:
// 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:
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:
// ✅ Good
return {
success: true,
data: { ... },
timestamp: new Date().toISOString()
};
// ❌ Avoid
return "Some data";4. Handle Sensitive Data
Never log or expose sensitive data:
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:
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
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
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
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:
{
"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
Related Steps
- HTTP Request - Make HTTP calls from the server
- Execute Node.js - Execute server-side JavaScript
- Ask to LLM - Process results with AI
- Condition - Branch based on tool results
Troubleshooting
Issue: Flow Not Pausing
Problem: Flow doesn't pause for client tool execution.
Solution:
- Ensure step type is
client-tool - Verify
toolNameis 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
clientToolsarray - 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
- Templating and Variables - Learn about template syntax
- Flow Steps Overview - All available step types
- JavaScript SDK Documentation - SDK client tools guide

