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\"}"
}Inputs
| 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) |
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:
{
"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, for example
variable_nameinside an expression block - Check that previous steps output the required variables
- Use
str(variable)inside an expression block 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

