Appearance
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:
- Register clients that can listen for events
- Create event listeners that trigger when specific events occur
- 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:
- Navigate to the Action Manager application in your administration panel
- Click "Create New Listener"
- Select the instance from the dropdown
- Select the event name from the dropdown (e.g.,
Canvas Opened
,File Converted
) - In the "Handler URL" field enter fully qualified URL for external webhook endpoints
- 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
Error Handling: Always implement proper error handling in your action handlers. Return meaningful error messages to help with debugging.
Performance: Action handlers should be designed to execute quickly. If an operation will take a long time, consider using a queuing system.
Idempotency: Design your handlers to be idempotent (can be called multiple times without changing the result).
Authentication: Always validate the provided
apiKey
andinstanceId
before performing operations.Logging: Implement detailed logging to help troubleshoot issues in production.
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:
- Create a test script that simulates an event payload
- Call your handler function directly with the test context
- 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:
- Check server logs for error messages
- Verify that your event listener is correctly registered
- Test the handler directly with a known payload
- Ensure all required fields are present in the context
- 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 Name | Explanation | Key Properties |
---|---|---|
Canvas Entered | (Canvas Opened) When a user opens or enters a canvas | canvas_id , user_id , timestamp |
Canvas Exited | (Canvas Closed) When a user closes or exits a canvas | canvas_id , user_id , duration |
Canvas File Downloaded | (Canvas File Downloaded) When a file is downloaded from a canvas | canvas_id , file_id , user_id |
Download Pitchmaster | (Download Pitchdeck) When a pitchmaster/pitchdeck is downloaded | canvas_id , file_id , format |
File Entered | (File Opened) When a user opens or enters a file | file_id , file_name , file_type |
File Exited | (File Closed) When a user closes or exits a file | file_id , user_id , duration |
File Published | (File Converted) When a file is published or converted | file_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 Name | Description | Required Properties |
---|---|---|
pia-search | Syncs a file to the PIA system for indexing and search | file_id |
auto-tag | Automatically tags files using AI analysis | file_id |
extract-pptx | Extracts slide content from PPTX files and updates file metadata | file_id |
id-proxy | Proxies requests to a specified AWS Lambda endpoint | method |
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: