Skip to main content

Overview

Tool approval responses are submitted through the standard Add Session Thread Message endpoint. The approval decisions are included in the content field of the message, alongside optional text feedback.

Request Format

Endpoint

POST /api/assistants/threads/{threadId}/messages

Authentication

const headers = {
  'Authorization': `Bearer ${API_KEY}`,
  'Content-Type': 'application/json',
  'X-Envole-User-Id': USER_ID
};

Basic Approval Response

const approvalResponse = {
  content: [
    {
      type: 'tool_approval_result',
      tool_approval_results: [
        {
          toolId: 'tool_123',
          toolName: 'google_calendar_create_event',
          toolProvider: 'GOOGLE_CALENDAR',
          toolCategory: 'CALENDAR',
          toolExecutionId: 'exec_123',
          toolExecutionBatchId: 'batch_456',
          toolMemoryId: 'mem_789',
          toolArguments: {
            event_title: 'Project Review Meeting',
            start_time: '2024-01-15T14:00:00Z',
            end_time: '2024-01-15T15:00:00Z',
            attendees: 'john@company.com,mary@company.com',
            location: 'Conference Room A'
          },
          approvalResult: 'ABORTED_WITH_FEEDBACK'
        }
      ]
    }
  ]
};

await fetch(`${API_BASE_URL}/api/assistants/threads/${threadId}/messages`, {
  method: 'POST',
  headers,
  body: JSON.stringify(approvalResponse)
});

Approval Result Structure

Each tool approval result must include all fields from the original approval request:
interface ToolApprovalResult {
  toolId: string;              // From original request
  toolName: string;            // From original request  
  toolProvider: string;        // From original request
  toolCategory: string;        // From original request
  toolExecutionId: string;     // From original request
  toolExecutionBatchId: string; // From original request
  toolMemoryId: string;        // From original request
  toolArguments: object;       // From original request
  approvalResult: 'ABORTED_WITH_FEEDBACK' | 'DENIED' | 'ABORTED';
}
Important: You must preserve all original fields from the TOOL_EXECUTION_APPROVAL_REQUEST event and only modify the approvalResult field.

Batch Response Examples

Mixed Approval/Denial

const mixedBatchResponse = {
  content: [
    {
      type: 'tool_approval_result',
      tool_approval_results: [
        {
          toolId: 'tool_123',
          toolName: 'send_email',
          toolProvider: 'GMAIL', 
          toolCategory: 'EMAIL',
          toolExecutionId: 'exec_123',
          toolExecutionBatchId: 'batch_456',
          toolMemoryId: 'mem_789',
          toolArguments: {
            to: 'user@company.com',
            subject: 'Project Update',
            body: 'Progress report attached'
          },
          approvalResult: 'ABORTED_WITH_FEEDBACK'
        },
        {
          toolId: 'tool_124',
          toolName: 'schedule_meeting',
          toolProvider: 'GOOGLE_CALENDAR',
          toolCategory: 'CALENDAR', 
          toolExecutionId: 'exec_124',
          toolExecutionBatchId: 'batch_456',
          toolMemoryId: 'mem_790',
          toolArguments: {
            title: 'Follow-up Meeting',
            date: '2024-01-16T10:00:00Z'
          },
          approvalResult: 'DENIED'
        }
      ]
    }
  ]
};

Abort with Feedback

const abortWithFeedback = {
  content: [
    {
      type: 'text',
      text: 'The email recipient list includes external contacts that should not receive this confidential information. Please revise the distribution list.'
    },
    {
      type: 'tool_approval_result',
      tool_approval_results: [
        {
          toolId: 'tool_123',
          toolName: 'send_email',
          toolProvider: 'GMAIL',
          toolCategory: 'EMAIL',
          toolExecutionId: 'exec_123', 
          toolExecutionBatchId: 'batch_456',
          toolMemoryId: 'mem_789',
          toolArguments: {
            to: 'external@competitor.com,internal@company.com',
            subject: 'Confidential Project Update',
            body: 'Internal strategy document attached'
          },
          approvalResult: 'ABORTED'
        },
        {
          toolId: 'tool_124', 
          toolName: 'save_draft',
          toolProvider: 'GMAIL',
          toolCategory: 'EMAIL',
          toolExecutionId: 'exec_124',
          toolExecutionBatchId: 'batch_456',
          toolMemoryId: 'mem_790', 
          toolArguments: {
            subject: 'Confidential Project Update',
            body: 'Internal strategy document attached'
          },
          approvalResult: 'ABORTED'
        }
      ]
    }
  ]
};

File and Image Restrictions

Allowed: Files with Abort Responses

const abortWithScreenshot = {
  content: [
    {
      type: 'text',
      text: 'The calendar event has the wrong timezone. See screenshot of the correct settings:'
    },
    {
      type: 'image',
      image_url: {
        url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...'
      }
    },
    {
      type: 'tool_approval_result',
      tool_approval_results: [
        {
          toolExecutionId: 'exec_123',
          toolExecutionBatchId: 'batch_456',
          // ... other required fields
          approvalResult: 'ABORTED'
        }
      ]
    }
  ]
};

Not Allowed: Files with Approval/Denial

// ❌ Files will be ignored for non-abort responses
const approvalWithFile = {
  content: [
    {
      type: 'image',
      image_url: { url: 'reference.png' }  // Will be ignored
    },
    {
      type: 'tool_approval_result',
      tool_approval_results: [
        {
          toolExecutionId: 'exec_123',
          approvalResult: 'ABORTED_WITH_FEEDBACK'  // Files ignored for approved/denied tools
        }
      ]
    }
  ]
};

Complete Implementation Example

class ToolApprovalHandler {
  private pendingApprovals = new Map<string, ToolApprovalRequest[]>();
  
  handleApprovalRequest(requests: ToolApprovalRequest[]) {
    // Group by batch ID
    const batchId = requests[0].toolExecutionBatchId;
    this.pendingApprovals.set(batchId, requests);
    
    // Show approval UI
    this.showApprovalInterface(requests);
  }
  
  async submitApprovalBatch(
    batchId: string, 
    decisions: Map<string, ApprovalDecision>,
    feedback?: string
  ) {
    const requests = this.pendingApprovals.get(batchId);
    if (!requests) {
      throw new Error(`No pending approvals for batch ${batchId}`);
    }
    
    // Build approval results
    const toolApprovalResults = requests.map(request => ({
      toolId: request.toolId,
      toolName: request.toolName,
      toolProvider: request.toolProvider,
      toolCategory: request.toolCategory,
      toolExecutionId: request.toolExecutionId,
      toolExecutionBatchId: request.toolExecutionBatchId,
      toolMemoryId: request.toolMemoryId,
      toolArguments: request.toolArguments,
      approvalResult: decisions.get(request.toolExecutionId)
    }));
    
    // Build content array
    const content = [];
    
    // Add feedback text if provided
    if (feedback) {
      content.push({
        type: 'text',
        text: feedback
      });
    }
    
    // Add approval results
    content.push({
      type: 'tool_approval_result',
      tool_approval_results: toolApprovalResults
    });
    
    // Submit to API
    const response = await fetch(
      `${API_BASE_URL}/api/assistants/threads/${this.threadId}/messages`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
          'X-Envole-User-Id': this.userId
        },
        body: JSON.stringify({ content })
      }
    );
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Approval submission failed: ${error.message}`);
    }
    
    // Clean up
    this.pendingApprovals.delete(batchId);
  }
  
  // Helper method for individual tool decisions
  setToolDecision(toolExecutionId: string, decision: ApprovalDecision) {
    // Find batch containing this tool
    let batchId: string | undefined;
    let batchRequests: ToolApprovalRequest[] | undefined;
    
    for (const [id, requests] of this.pendingApprovals.entries()) {
      if (requests.some(req => req.toolExecutionId === toolExecutionId)) {
        batchId = id;
        batchRequests = requests;
        break;
      }
    }
    
    if (!batchId || !batchRequests) {
      throw new Error(`Tool ${toolExecutionId} not found in pending approvals`);
    }
    
    // Update UI state
    this.updateToolDecision(toolExecutionId, decision);
    
    // Check if batch is complete
    const allDecisions = this.getAllBatchDecisions(batchId);
    if (allDecisions.size === batchRequests.length) {
      // All tools decided - ready to submit
      this.enableBatchSubmission(batchId);
    }
  }
}

Response Handling

The API responds with standard SSE events. After submitting approvals, you’ll receive:
  1. NOTIFICATION_TOOL_EXECUTION_APPROVAL_ACCEPTED for approved tools
  2. NOTIFICATION_TOOL_EXECUTION_APPROVAL_DENIED for denied tools
  3. NOTIFICATION_TOOL_EXECUTION_APPROVAL_ABORTED for aborted tools
  4. Standard AGENT_RESPONSE_* events as the agent continues processing

Error Responses

Validation Errors

{
  "error": "Invalid tool approval batch",
  "details": {
    "batchId": "batch_456", 
    "issues": [
      {
        "toolExecutionId": "exec_123",
        "error": "Missing required field: toolArguments"
      },
      {
        "toolExecutionId": "exec_124", 
        "error": "Invalid approvalResult: must be ABORTED_WITH_FEEDBACK, DENIED, or ABORTED"
      }
    ]
  }
}

State Constraint Violations

{
  "error": "Invalid approval batch: cannot mix ABORTED with other approval states",
  "batchId": "batch_456",
  "conflictingStates": ["ABORTED_WITH_FEEDBACK", "ABORTED"]
}

Next Steps

I