Skip to content

Action Manager Handlers

This guide explains how to implement custom action handlers that integrate with the Core API's action management system. Action handlers provide a powerful way to extend your application's functionality by responding to specific events or triggers.

Overview

The Action Manager is a system that allows you to:

  1. Register clients that can listen for events
  2. Create event listeners that trigger when specific events occur
  3. Execute custom action handlers to process events with your own business logic

Action handlers are server-side functions that are executed when specific events are triggered. They receive context about the event and can perform custom processing, integrations, or data manipulations.

Key Benefits

  • Extensibility: Add new functionality without modifying the core application
  • Modularity: Create independent, reusable components for specific tasks
  • Integration: Connect with external systems and services
  • Automation: Trigger workflows based on application events
  • Customization: Implement organization-specific business logic

Action Handler Interface

All action handlers must implement the ActionHandlerContext interface:

typescript
interface ActionHandlerContext {
  payload: any;        // The event payload
  apiKey: string;      // API key for authentication
  instanceId: string;  // ID of the instance
  orgDomain: string;   // Organization domain
  orgDomainName: string; // Organization domain name
  headers?: Headers;   // Original request headers
}

And should return a result conforming to the ActionResult interface:

typescript
interface ActionResult {
  success: boolean;    // Whether the action was successful
  message: string;     // A human-readable message about the result
  data?: any;          // Optional data returned by the handler
  error?: string;      // Error message if success is false
}

Implementing Custom Action Handlers

Create your handler function

Create a function that accepts an ActionHandlerContext and returns a Promise resolving to an ActionResult.

Important: For external webhook URLs, Pitcher always calls the handler URL with a POST request, passing the event context in the request body.

Here's an example handler that processes image optimization:

typescript
async function handleImageOptimization(
  context: ActionHandlerContext
): Promise<ActionResult> {
  try {
    const { payload, apiKey, instanceId } = context;
    
    // Extract file ID from the event payload
    const fileId = payload?.properties?.file_id;
    if (!fileId) {
      return {
        success: false,
        message: 'Missing file ID in event payload',
        error: 'file_id property is required in the event properties',
      };
    }
    
    // Your image optimization logic here
    // ...
    
    return {
      success: true,
      message: 'Image optimization completed successfully',
      data: {
        fileId,
        compressionRate: '60%', // Example data
      },
    };
  } catch (error) {
    return {
      success: false,
      message: 'Failed to optimize image',
      error: (error as Error).message,
    };
  }
}

Examples in Different Languages

Node.js Example

javascript
// handlers/imageOptimization.js
const sharp = require('sharp');
const fetch = require('node-fetch');

/**
 * Handler for image optimization
 * @param {Object} context - The action context
 * @returns {Promise<Object>} - Result of the operation
 */
async function handleImageOptimization(context) {
  try {
    const { payload, apiKey, instanceId } = context;
    
    // Extract file ID from the event payload
    const fileId = payload?.properties?.file_id;
    if (!fileId) {
      return {
        success: false,
        message: 'Missing file ID in event payload',
        error: 'file_id property is required in the event properties',
      };
    }
    
    // Get file details from your API
    const fileResponse = await fetch(`https://api.example.com/files/${fileId}`, {
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'x-instance-id': instanceId,
      },
    });
    
    if (!fileResponse.ok) {
      throw new Error(`Failed to fetch file: ${fileResponse.statusText}`);
    }
    
    const file = await fileResponse.json();
    const imageUrl = file.download_url;
    
    // Download the image
    const imageResponse = await fetch(imageUrl);
    const imageBuffer = await imageResponse.arrayBuffer();
    
    // Optimize the image with sharp
    const optimizedBuffer = await sharp(Buffer.from(imageBuffer))
      .resize(1200, null, { withoutEnlargement: true })
      .jpeg({ quality: 80 })
      .toBuffer();
    
    // Upload the optimized image
    const formData = new FormData();
    formData.append('file', new Blob([optimizedBuffer]), 'optimized.jpg');
    formData.append('fileId', fileId);
    
    const uploadResponse = await fetch('https://api.example.com/files/optimized', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'x-instance-id': instanceId,
      },
      body: formData,
    });
    
    if (!uploadResponse.ok) {
      throw new Error(`Failed to upload optimized image: ${uploadResponse.statusText}`);
    }
    
    const uploadResult = await uploadResponse.json();
    
    return {
      success: true,
      message: 'Image optimization completed successfully',
      data: {
        fileId,
        optimizedFileId: uploadResult.id,
        originalSize: file.size,
        optimizedSize: uploadResult.size,
        compressionRate: `${Math.round((1 - (uploadResult.size / file.size)) * 100)}%`,
      },
    };
  } catch (error) {
    return {
      success: false,
      message: 'Failed to optimize image',
      error: error.message,
    };
  }
}

module.exports = handleImageOptimization;

PHP Example

php
<?php
/**
 * Handler for document conversion to PDF
 * 
 * @param array $context Action context containing payload and authentication info
 * @return array Result of the operation
 */
function handleDocumentToPdf($context) {
    try {
        $payload = $context['payload'];
        $apiKey = $context['apiKey'];
        $instanceId = $context['instanceId'];
        
        // Extract file ID from the event payload
        $fileId = $payload['properties']['file_id'] ?? null;
        if (!$fileId) {
            return [
                'success' => false,
                'message' => 'Missing file ID in event payload',
                'error' => 'file_id property is required in the event properties',
            ];
        }
        
        // Get file details from API
        $ch = curl_init("https://api.example.com/files/{$fileId}");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            "Authorization: Bearer {$apiKey}",
            "x-instance-id: {$instanceId}",
            "Content-Type: application/json"
        ]);
        
        $response = curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($statusCode !== 200) {
            throw new Exception("Failed to fetch file, status code: {$statusCode}");
        }
        
        $file = json_decode($response, true);
        $downloadUrl = $file['download_url'];
        
        // Download the file
        $fileContent = file_get_contents($downloadUrl);
        if ($fileContent === false) {
            throw new Exception("Failed to download file from URL");
        }
        
        // Call external conversion service
        $convertApiUrl = "https://convert.example.com/to-pdf";
        $boundary = uniqid();
        $postData = "--{$boundary}\r\n" .
                    "Content-Disposition: form-data; name=\"file\"; filename=\"document.docx\"\r\n" .
                    "Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document\r\n\r\n" .
                    $fileContent . "\r\n" .
                    "--{$boundary}--\r\n";
        
        $ch = curl_init($convertApiUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            "Content-Type: multipart/form-data; boundary={$boundary}",
            "Authorization: Bearer {$apiKey}"
        ]);
        
        $pdfResponse = curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($statusCode !== 200) {
            throw new Exception("Failed to convert document, status code: {$statusCode}");
        }
        
        // Upload the converted PDF
        $uploadApiUrl = "https://api.example.com/files/upload";
        $boundary = uniqid();
        $postData = "--{$boundary}\r\n" .
                    "Content-Disposition: form-data; name=\"file\"; filename=\"converted.pdf\"\r\n" .
                    "Content-Type: application/pdf\r\n\r\n" .
                    $pdfResponse . "\r\n" .
                    "--{$boundary}\r\n" .
                    "Content-Disposition: form-data; name=\"original_file_id\"\r\n\r\n" .
                    $fileId . "\r\n" .
                    "--{$boundary}--\r\n";
        
        $ch = curl_init($uploadApiUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            "Content-Type: multipart/form-data; boundary={$boundary}",
            "Authorization: Bearer {$apiKey}",
            "x-instance-id: {$instanceId}"
        ]);
        
        $uploadResponse = curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($statusCode !== 200) {
            throw new Exception("Failed to upload converted PDF, status code: {$statusCode}");
        }
        
        $uploadResult = json_decode($uploadResponse, true);
        
        return [
            'success' => true,
            'message' => 'Document conversion completed successfully',
            'data' => [
                'fileId' => $fileId,
                'pdfFileId' => $uploadResult['id'],
                'fileName' => $uploadResult['name'],
            ],
        ];
    } catch (Exception $e) {
        return [
            'success' => false,
            'message' => 'Failed to convert document to PDF',
            'error' => $e->getMessage(),
        ];
    }
}

Python Example

python
import requests
import json
from typing import Dict, Any

def handle_document_classification(context: Dict[str, Any]) -> Dict[str, Any]:
    """
    Handler for document classification using AI
    
    Args:
        context: The action context containing payload and auth information
        
    Returns:
        Dict containing the result of the operation
    """
    try:
        payload = context.get('payload', {})
        api_key = context.get('apiKey')
        instance_id = context.get('instanceId')
        
        # Extract file ID from the event payload
        file_id = payload.get('properties', {}).get('file_id')
        if not file_id:
            return {
                'success': False,
                'message': 'Missing file ID in event payload',
                'error': 'file_id property is required in the event properties'
            }
        
        # Get file details from API
        headers = {
            'Authorization': f'Bearer {api_key}',
            'x-instance-id': instance_id,
            'Content-Type': 'application/json'
        }
        
        file_response = requests.get(
            f'https://api.example.com/files/{file_id}',
            headers=headers
        )
        
        if file_response.status_code != 200:
            raise Exception(f'Failed to fetch file: {file_response.status_code}')
        
        file_data = file_response.json()
        download_url = file_data.get('download_url')
        
        # Download the file content
        file_content_response = requests.get(download_url)
        if file_content_response.status_code != 200:
            raise Exception('Failed to download file content')
        
        # Call AI classification service
        classification_api_url = 'https://ai.example.com/classify'
        classification_payload = {
            'content': file_content_response.text,
            'filename': file_data.get('name', ''),
            'file_type': file_data.get('mime_type', '')
        }
        
        classification_response = requests.post(
            classification_api_url,
            json=classification_payload,
            headers={'Authorization': f'Bearer {api_key}'}
        )
        
        if classification_response.status_code != 200:
            raise Exception(f'Classification service failed: {classification_response.status_code}')
        
        classification_result = classification_response.json()
        
        # Update file metadata with classification results
        categories = classification_result.get('categories', [])
        confidence = classification_result.get('confidence', 0.0)
        
        update_payload = {
            'metadata': {
                'ai_classification': {
                    'categories': categories,
                    'confidence': confidence,
                    'processed_at': classification_result.get('timestamp')
                }
            },
            'tags': categories[:3]  # Add top 3 categories as tags
        }
        
        update_response = requests.patch(
            f'https://api.example.com/files/{file_id}',
            json=update_payload,
            headers=headers
        )
        
        if update_response.status_code != 200:
            raise Exception(f'Failed to update file metadata: {update_response.status_code}')
        
        return {
            'success': True,
            'message': 'Document classification completed successfully',
            'data': {
                'fileId': file_id,
                'categories': categories,
                'confidence': confidence,
                'tags': categories[:3]
            }
        }
        
    except Exception as e:
        return {
            'success': False,
            'message': 'Failed to classify document',
            'error': str(e)
        }

Registering an Event Listener

Once you've implemented your custom action handler, you need to create an event listener to trigger it when specific events occur. You can do this either programmatically via the API or using the Action Manager GUI.

Option 1: Using the Action Manager GUI

The Action Manager provides a user-friendly interface for creating and managing event listeners:

  1. Navigate to the Action Manager application in your administration panel
  2. Click "Create New Listener"
  3. Select the instance from the dropdown
  4. Select the event name from the dropdown (e.g., Canvas Opened, File Converted)
  5. In the "Handler URL" field enter fully qualified URL for external webhook endpoints
  6. Click "Save" to register the listener

The Action Manager GUI automatically adds links to your documentation when it detects custom action handlers in your code. These links appear in the UI next to the handler selection dropdown, providing users with easy access to your documentation.

This approach is recommended for non-technical users or for testing purposes.

Option 2: Using the API

You can also register listeners programmatically using the API:

javascript
// Register an event listener
const registerListener = async (instanceId, eventName, handlerName) => {
  // Note: For external webhook URLs, Pitcher makes POST requests with the event context in the body
  const response = await fetch('/core/api/protected/actions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer YOUR_API_KEY'
    },
    body: JSON.stringify({
      action: 'create-listener',
      instanceId: instanceId,
      eventName: eventName,
      url: handlerName  // Action name or external webhook URL
    }),
  });
  
  if (!response.ok) {
    throw new Error(`Failed to register listener: ${response.statusText}`);
  }
  
  return await response.json();
};

// Example usage
registerListener(
  'my-instance-123', 
  'file_uploaded',  // Event to listen for
  'optimize-image'  // Handler to execute
);

API Examples Using Pitcher SDK

Here are examples using the official Pitcher SDK for various languages:

JavaScript/Node.js

javascript
const { PitcherApiClient } = require('@pitcher/api-client');

const client = new PitcherApiClient({
  apiKey: 'YOUR_API_KEY',
  instanceId: 'YOUR_INSTANCE_ID'
});

// Register a listener
async function createEventListener() {
  try {
    const result = await client.post('/core/api/protected/actions', {
      action: 'create-listener',
      instanceId: 'YOUR_INSTANCE_ID',
      eventName: 'file_uploaded',
      url: 'optimize-image'
    });
    
    console.log('Listener created:', result.id);
    return result;
  } catch (error) {
    console.error('Failed to create listener:', error);
  }
}

createEventListener();

PHP

php
<?php
require_once 'vendor/autoload.php';

use Pitcher\ApiClient\Client;

$client = new Client([
  'api_key' => 'YOUR_API_KEY',
  'instance_id' => 'YOUR_INSTANCE_ID'
]);

// Register a listener
try {
  $response = $client->post('/core/api/protected/actions', [
    'action' => 'create-listener',
    'instanceId' => 'YOUR_INSTANCE_ID',
    'eventName' => 'file_uploaded',
    'url' => 'optimize-image'
  ]);
  
  echo "Listener created: " . $response['id'];
} catch (Exception $e) {
  echo "Error: " . $e->getMessage();
}

Python

python
from pitcher_api import PitcherClient

client = PitcherClient(
    api_key="YOUR_API_KEY",
    instance_id="YOUR_INSTANCE_ID"
)

# Register a listener
try:
    response = client.post("/core/api/protected/actions", {
        "action": "create-listener",
        "instanceId": "YOUR_INSTANCE_ID",
        "eventName": "file_uploaded",
        "url": "optimize-image"
    })
    
    print(f"Listener created: {response['id']}")
except Exception as e:
    print(f"Error: {str(e)}")

Best Practices

  1. Error Handling: Always implement proper error handling in your action handlers. Return meaningful error messages to help with debugging.

  2. Performance: Action handlers should be designed to execute quickly. If an operation will take a long time, consider using a queuing system.

  3. Idempotency: Design your handlers to be idempotent (can be called multiple times without changing the result).

  4. Authentication: Always validate the provided apiKey and instanceId before performing operations.

  5. Logging: Implement detailed logging to help troubleshoot issues in production.

  6. Multiple Handlers: You can register multiple handlers for the same event by using the pipe (|) character in the URL:

    url: 'optimize-image|auto-tag|pia-search'

Common Use Cases

  • Document Processing: Convert, OCR, or extract data from uploaded documents
  • Media Transformation: Resize images, transcode videos, or extract audio
  • Notification Systems: Send emails, SMS, or push notifications on specific events
  • Data Synchronization: Keep external systems in sync with your application
  • AI Processing: Run documents through AI services for classification, summarization, or analysis
  • Analytics Integration: Forward events to analytics platforms for tracking and reporting
  • Workflow Automation: Trigger complex business processes when specific events occur
  • Regulatory Compliance: Automatically process and archive documents according to compliance requirements

Testing Your Action Handlers

To test your action handlers locally:

  1. Create a test script that simulates an event payload
  2. Call your handler function directly with the test context
  3. Verify the returned result matches your expectations

Example test script:

javascript
const { handleImageOptimization } = require('./handlers/imageOptimization');

async function testHandler() {
  const testContext = {
    payload: {
      event: 'file_uploaded',
      properties: {
        file_id: 'test-file-123',
        file_type: 'image/jpeg'
      }
    },
    apiKey: 'test-api-key',
    instanceId: 'test-instance',
    orgDomain: 'test.example.com',
    orgDomainName: 'Test Example Name',
    headers: new Headers()
  };
  
  const result = await handleImageOptimization(testContext);
  console.log('Handler result:', result);
}

testHandler().catch(console.error);

Debugging Action Handlers

When your action handler isn't working as expected:

  1. Check server logs for error messages
  2. Verify that your event listener is correctly registered
  3. Test the handler directly with a known payload
  4. Ensure all required fields are present in the context
  5. Verify API keys and permissions are correctly set up

Available Events

You can create listeners for the following common events (the event explanation is shown in parentheses):

Event NameExplanationKey Properties
Canvas Entered(Canvas Opened) When a user opens or enters a canvascanvas_id, user_id, timestamp
Canvas Exited(Canvas Closed) When a user closes or exits a canvascanvas_id, user_id, duration
Canvas File Downloaded(Canvas File Downloaded) When a file is downloaded from a canvascanvas_id, file_id, user_id
Download Pitchmaster(Download Pitchdeck) When a pitchmaster/pitchdeck is downloadedcanvas_id, file_id, format
File Entered(File Opened) When a user opens or enters a filefile_id, file_name, file_type
File Exited(File Closed) When a user closes or exits a filefile_id, user_id, duration
File Published(File Converted) When a file is published or convertedfile_id, source_format, target_format

These event names should be used exactly as shown in the "Event Name" column when creating your event listeners. The Action Manager GUI will display these events in a dropdown menu for selection when creating a new listener.

For a complete list of available events, refer to the Event Documentation in the Action Manager application UI.

Additional Resources

Built-in Action Handlers

The system provides several built-in action handlers that you can use:

Handler NameDescriptionRequired Properties
pia-searchSyncs a file to the PIA system for indexing and searchfile_id
auto-tagAutomatically tags files using AI analysisfile_id
extract-pptxExtracts slide content from PPTX files and updates file metadatafile_id
id-proxyProxies requests to a specified AWS Lambda endpointmethod

These handlers can be referenced directly by name when creating event listeners, providing ready-to-use functionality without implementing custom code.

Triggering Events Programmatically

In addition to events that are triggered automatically by the system, you can programmatically trigger events using the API:

javascript
const triggerEvent = async (instanceId, eventName, properties) => {
  const response = await fetch('/core/api/protected/actions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer YOUR_API_KEY'
    },
    body: JSON.stringify({
      action: 'trigger-event',
      event: {
        event: eventName,
        properties: {
          instance_id: instanceId,
          organization_id: 'your-org-domain.my.pitcher.com',
          ...properties
        }
      }
    }),
  });
  
  if (!response.ok) {
    throw new Error(`Failed to trigger event: ${response.statusText}`);
  }
  
  return await response.json();
};

This allows you to integrate with the Action Manager from your custom code and trigger handlers based on your application's specific business logic.

Special URL Formats

The Action Manager supports a special URL format for configuring listeners:

ID Proxy URLs

You can use the id:// URL scheme to route events to AWS Lambda functions: