# TaskNotes Webhooks TaskNotes webhooks enable real-time integrations by sending HTTP POST requests to your configured endpoints whenever specific events occur. This allows you to build automation, sync with external services, and create custom workflows. ## Quick Start 1. **Enable HTTP API** in TaskNotes Settings → HTTP API tab 2. **Add a webhook** by clicking "Add Webhook" in the webhook settings 3. **Select events** you want to receive notifications for 4. **Configure transformation** (optional) for custom payload formats 5. **Test your integration** using the included test server or your endpoint ## Webhook Management Interface TaskNotes provides a modern, intuitive interface for managing webhooks: ### Settings Interface - **Card-based layout** - Each webhook displayed as a clean card with clear status indicators - **Visual status indicators** - Active webhooks show green checkmarks, inactive show red X - **Real-time statistics** - Success and failure counts with color-coded icons - **Action buttons** - Enable/disable and delete webhooks with confirmation dialogs ### Adding Webhooks The webhook creation modal provides: - **Event selection** - Choose from all available events with descriptions - **Transform configuration** - Optional JavaScript/JSON file specification - **Headers control** - Toggle custom headers for strict CORS services - **Validation** - Real-time URL and event validation ### Status Monitoring Each webhook card displays: - **Connection status** - Visual indicators for active/inactive state - **Success metrics** - Green checkmark with success count - **Failure metrics** - Red X with failure count - **Transform info** - Shows configured transformation file - **CORS status** - Warning when custom headers are disabled ### Accessibility Features The webhook interface is designed for accessibility: - **Semantic icons** - Uses Obsidian's icon system instead of emoji characters - **Descriptive labels** - Clear ARIA labels for screen readers - **Keyboard navigation** - Full keyboard support for all interactions - **High contrast** - Status indicators use color-coded backgrounds with icons - **Tooltips** - Helpful hover text for all action buttons - **Focus states** - Clear focus indicators for keyboard users ## Webhook Events TaskNotes triggers webhooks for the following events: ### Task Events - `task.created` - When a new task is created - `task.updated` - When a task is modified - `task.deleted` - When a task is removed - `task.completed` - When a task status changes to completed - `task.archived` - When a task is archived - `task.unarchived` - When a task is unarchived ### Time Tracking Events - `time.started` - When time tracking starts on a task - `time.stopped` - When time tracking stops on a task ### Pomodoro Events - `pomodoro.started` - When a pomodoro session begins - `pomodoro.completed` - When a pomodoro session finishes successfully - `pomodoro.interrupted` - When a pomodoro session is interrupted ### Recurring Task Events - `recurring.instance.completed` - When a recurring task instance is marked complete ### Reminder Events - `reminder.triggered` - When a task reminder fires and displays a notification ## Payload Structure All webhook payloads follow this structure: ```json { "event": "task.created", "timestamp": "2024-03-15T14:30:00.000Z", "vault": { "name": "My Vault", "path": "/Users/username/Documents/MyVault" }, "data": { "task": { "id": "path/to/task.md", "title": "Review PR #123", "status": "todo", "priority": "high", "due": "2024-03-16", "scheduled": null, "path": "path/to/task.md", "archived": false, "tags": ["123"], "contexts": ["work"], "projects": ["[[Project Name]]"], "timeEstimate": 60 } } } ``` ## Event-Specific Payloads ### Task Created/Updated/Deleted ```json { "event": "task.created", "data": { "task": { /* TaskInfo object */ } } } ``` ### Task Updated (includes previous state) ```json { "event": "task.updated", "data": { "task": { /* Current TaskInfo */ }, "previous": { /* Previous TaskInfo */ } } } ``` ### Time Tracking Events ```json { "event": "time.started", "data": { "task": { /* TaskInfo object */ }, "session": { "startTime": "2024-03-15T14:30:00.000Z", "endTime": null, "description": null } } } ``` ### NLP Task Creation ```json { "event": "task.created", "data": { "task": { /* TaskInfo object */ }, "source": "nlp", "originalText": "Review PR #123 tomorrow high priority @work" } } ``` ### Reminder Events ```json { "event": "reminder.triggered", "data": { "task": { /* TaskInfo object */ }, "reminder": { "id": "rem_1234", "type": "relative", "relatedTo": "due", "offset": "-PT15M", "description": "Don't forget this important task!" }, "notificationTime": "2024-03-15T14:15:00.000Z", "message": "Don't forget this important task!", "notificationType": "system" } } ``` ## Security ### Webhook Signatures TaskNotes signs all webhook payloads using HMAC-SHA256. Verify signatures to ensure authenticity: **Headers:** - `X-TaskNotes-Event`: Event type (e.g., "task.created") - `X-TaskNotes-Signature`: HMAC signature (hex-encoded) - `X-TaskNotes-Delivery-ID`: Unique delivery ID **Verification (Node.js):** ```javascript const crypto = require('crypto'); function verifyWebhook(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(JSON.stringify(payload)) .digest('hex'); return signature === expectedSignature; } // Express.js example app.post('/webhook', express.json(), (req, res) => { const signature = req.headers['x-tasknotes-signature']; const isValid = verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET); if (!isValid) { return res.status(401).send('Invalid signature'); } // Process webhook... console.log('Event:', req.body.event); res.status(200).send('OK'); }); ``` **Verification (Python):** ```python import hmac import hashlib import json def verify_webhook(payload, signature, secret): expected = hmac.new( secret.encode('utf-8'), json.dumps(payload).encode('utf-8'), hashlib.sha256 ).hexdigest() return signature == expected # Flask example from flask import Flask, request @app.route('/webhook', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-TaskNotes-Signature') if not verify_webhook(request.json, signature, WEBHOOK_SECRET): return 'Invalid signature', 401 event = request.json['event'] # Process webhook... return 'OK' ``` ## Integration Examples ### Todoist Sync ```javascript app.post('/webhook/tasknotes', (req, res) => { const { event, data } = req.body; if (event === 'task.created') { // Create task in Todoist await todoist.createTask({ content: data.task.title, due_date: data.task.due, priority: mapPriority(data.task.priority), project_id: getProjectId(data.task.projects[0]) }); } res.status(200).send('OK'); }); ``` ### Slack Notifications ```javascript app.post('/webhook/tasknotes', (req, res) => { const { event, data } = req.body; if (event === 'task.completed') { slack.chat.postMessage({ channel: '#productivity', text: `✅ Task completed: ${data.task.title}` }); } res.status(200).send('OK'); }); ``` ### Time Tracking Integration ```javascript app.post('/webhook/tasknotes', (req, res) => { const { event, data } = req.body; if (event === 'time.started') { // Start timer in external service await toggl.startTimer({ description: data.task.title, project: data.task.projects[0] }); } else if (event === 'time.stopped') { await toggl.stopTimer(); } res.status(200).send('OK'); }); ``` ### Reminder Notifications ```javascript app.post('/webhook/tasknotes', (req, res) => { const { event, data } = req.body; if (event === 'reminder.triggered') { // Forward reminder to mobile app via push notification await pushNotification.send({ title: 'TaskNotes Reminder', body: data.message, data: { taskId: data.task.id, taskTitle: data.task.title, reminderType: data.reminder.type } }); // Or send to smart home system await homeAssistant.notify({ message: data.message, service: 'mobile_app_phone' }); } res.status(200).send('OK'); }); ``` ### Analytics Dashboard ```javascript app.post('/webhook/tasknotes', (req, res) => { const { event, data, vault } = req.body; // Store event in database await db.events.create({ vault_name: vault.name, event_type: event, task_id: data.task?.id, timestamp: new Date(), metadata: data }); // Emit to real-time dashboard io.emit('task-update', { event, data }); res.status(200).send('OK'); }); ``` ## Webhook Management ### Register Webhook (via API) ```bash curl -X POST http://localhost:8080/api/webhooks \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -d '{ "url": "https://your-service.com/webhook", "events": ["task.created", "task.completed"] }' ``` ### List Webhooks ```bash curl http://localhost:8080/api/webhooks \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ### Delete Webhook ```bash curl -X DELETE http://localhost:8080/api/webhooks/WEBHOOK_ID \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ### View Delivery History ```bash curl http://localhost:8080/api/webhooks/deliveries \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ## Reliability Features ### Automatic Retries - Failed deliveries retry 3 times with exponential backoff (1s, 2s, 4s) - Webhooks automatically disabled after 10+ consecutive failures ### Delivery Tracking - Each delivery gets a unique ID for tracking - Success/failure counts maintained per webhook - Recent delivery history available via API ### Error Handling - 10-second timeout per delivery attempt - HTTP status codes tracked for debugging - Failed webhooks don't block TaskNotes operations ## Payload Transformations ### Transform Files TaskNotes supports custom payload transformations using JavaScript or JSON template files stored in your vault. This allows you to adapt webhook payloads to match the specific format required by different services (Discord, Slack, custom APIs, etc.). 📁 **[View Transform Examples](examples/webhook-transforms/)** - Ready-to-use transform files for Discord, Slack, Teams, and more! #### How Transform Files Work 1. **File Location**: Transform files must be stored in your Obsidian vault 2. **File Types**: Supports `.js` (JavaScript) and `.json` (JSON template) files 3. **Execution**: Files are read and executed when a webhook is triggered 4. **Safety**: JavaScript files run in a controlled context for security 5. **Error Handling**: Failed transformations fall back to original payload #### JavaScript Transformations JavaScript files provide maximum flexibility for complex transformations. The file must define a `transform` function that receives the webhook payload and returns the transformed data. **Basic Structure:** ```javascript function transform(payload) { // Your transformation logic here return transformedPayload; } ``` **Complete Discord Example:** ```javascript // discord-webhook.js - Transform for Discord webhook format function transform(payload) { const { event, data, timestamp, vault } = payload; // Handle different event types if (event === 'task.completed') { return { embeds: [{ title: "✅ Task Completed", description: data.task.title, color: 5763719, // Green color fields: [ { name: "Priority", value: data.task.priority || "Normal", inline: true }, { name: "Project", value: data.task.projects?.[0] || "None", inline: true }, { name: "Due Date", value: data.task.due || "Not set", inline: true } ], footer: { text: `From ${vault.name}`, icon_url: "https://obsidian.md/favicon.ico" }, timestamp: timestamp }] }; } else if (event === 'task.created') { return { embeds: [{ title: "📝 New Task Created", description: data.task.title, color: 3447003, // Blue color fields: [ { name: "Status", value: data.task.status, inline: true }, { name: "Priority", value: data.task.priority || "Normal", inline: true } ], timestamp: timestamp }] }; } else if (event === 'pomodoro.completed') { return { embeds: [{ title: "🍅 Pomodoro Completed", description: `Finished working on: ${data.task.title}`, color: 15158332, // Red color timestamp: timestamp }] }; } // For unhandled events, return a generic message return { content: `TaskNotes: ${event} event triggered` }; } ``` **Slack Example:** ```javascript // slack-webhook.js - Transform for Slack webhook format function transform(payload) { const { event, data, vault } = payload; if (event === 'task.completed') { return { text: `Task completed: ${data.task.title}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `✅ *Task Completed*\n${data.task.title}` } }, { type: "context", elements: [ { type: "mrkdwn", text: `Priority: ${data.task.priority || 'Normal'} | Vault: ${vault.name}` } ] } ] }; } return { text: `TaskNotes: ${event} in ${vault.name}` }; } ``` #### JSON Templates JSON templates provide a simpler way to transform payloads using variable substitution. Templates can define different formats for different events. **Template Structure:** ```json { "event-name": { /* Template for specific event */ }, "default": { /* Fallback template for all other events */ } } ``` **Variable Syntax:** - Use `${path.to.value}` to insert values from the payload - Supports nested object access (e.g., `${data.task.title}`) - Variables that don't exist remain as literal text **Slack Template Example:** ```json { "task.completed": { "text": "Task completed: ${data.task.title}", "channel": "#tasks", "username": "TaskNotes", "icon_emoji": ":white_check_mark:", "attachments": [ { "color": "good", "fields": [ { "title": "Priority", "value": "${data.task.priority}", "short": true }, { "title": "Project", "value": "${data.task.projects.0}", "short": true } ] } ] }, "task.created": { "text": "New task: ${data.task.title}", "channel": "#tasks", "username": "TaskNotes", "icon_emoji": ":memo:" }, "default": { "text": "TaskNotes event: ${event}", "channel": "#general", "username": "TaskNotes" } } ``` **Teams Template Example:** ```json { "task.completed": { "@type": "MessageCard", "@context": "http://schema.org/extensions", "themeColor": "28a745", "summary": "Task Completed", "sections": [ { "activityTitle": "✅ Task Completed", "activitySubtitle": "${data.task.title}", "facts": [ { "name": "Priority:", "value": "${data.task.priority}" }, { "name": "Vault:", "value": "${vault.name}" } ] } ] }, "default": { "@type": "MessageCard", "@context": "http://schema.org/extensions", "summary": "TaskNotes Event", "text": "Event ${event} triggered in ${vault.name}" } } ``` #### Advanced JavaScript Examples **Conditional Logic:** ```javascript function transform(payload) { const { event, data } = payload; // Only process high priority tasks if (data.task && data.task.priority !== 'high') { return null; // Return null to skip webhook delivery } // Custom logic based on task properties if (data.task.tags && data.task.tags.includes('urgent')) { return { priority: "high", message: `🚨 URGENT: ${data.task.title}`, event: event }; } return payload; // Return original } ``` **Data Enrichment:** ```javascript function transform(payload) { const { event, data } = payload; // Add computed fields const enrichedPayload = { ...payload, computed: { isOverdue: data.task.due && new Date(data.task.due) < new Date(), hasProject: data.task.projects && data.task.projects.length > 0, estimatedMinutes: data.task.timeEstimate || 0, daysSinceDue: data.task.due ? Math.floor((new Date() - new Date(data.task.due)) / (1000 * 60 * 60 * 24)) : null } }; return enrichedPayload; } ``` **Multi-Service Routing:** ```javascript function transform(payload) { const { event, data } = payload; // Return array to send to multiple endpoints const results = []; // Always log to analytics service results.push({ service: "analytics", event: event, task_id: data.task.id, timestamp: new Date().toISOString() }); // Send high priority tasks to alert channel if (data.task.priority === 'high') { results.push({ service: "alerts", text: `High priority task: ${data.task.title}`, urgency: "high" }); } return results; } ``` #### Security Considerations - **Sandboxed Execution**: JavaScript files run in a controlled context - **No Node.js APIs**: Transform functions cannot access file system, network, or other Node.js APIs - **Error Isolation**: Transform errors don't affect TaskNotes or other webhooks - **Input Validation**: Always validate payload structure in your transform function #### Debugging Transform Files **Console Logging:** Transform functions cannot use `console.log()`, but you can return debug information: ```javascript function transform(payload) { try { // Your transformation logic const result = { /* transformed data */ }; return { ...result, _debug: { originalEvent: payload.event, transformedAt: new Date().toISOString() } }; } catch (error) { // Return error info in payload for debugging return { error: error.message, originalPayload: payload }; } } ``` **Testing Strategy:** 1. Start with a simple transform that returns the original payload 2. Add small changes incrementally 3. Use webhook.site or the built-in test server to inspect outputs 4. Check TaskNotes console for transformation errors ### Headers Configuration TaskNotes includes custom headers by default: - `X-TaskNotes-Event`: Event type - `X-TaskNotes-Signature`: HMAC signature - `X-TaskNotes-Delivery-ID`: Unique delivery ID For services with strict CORS policies (Discord, Slack), disable custom headers in webhook settings. ## Testing Webhooks ### Built-in Test Server TaskNotes includes a comprehensive test server for webhook development: ```bash # Navigate to TaskNotes directory node test-webhook.js # Or specify custom port node test-webhook.js 8080 ``` The test server provides: - **Real-time payload inspection** with formatted output - **Signature verification** using configurable test secret - **Event-specific processing** with detailed logging - **CORS support** for browser-based testing - **Health check endpoint** at `/health` #### Configuration Use the test secret when adding the webhook: ``` URL: http://localhost:3000/webhook Secret: test-secret-key-for-tasknotes-webhooks ``` ### Local Testing with ngrok ```bash # Install ngrok npm install -g ngrok # Expose local server ngrok http 3000 # Use the ngrok URL in webhook settings # https://abc123.ngrok.io/webhook ``` ### Webhook Testing Service Use services like [webhook.site](https://webhook.site) for quick testing: 1. Go to webhook.site and copy your unique URL 2. Add it as a webhook in TaskNotes 3. Perform actions to trigger events 4. View payloads in real-time on webhook.site ## Best Practices ### Security - Always verify webhook signatures - Use HTTPS endpoints in production - Store webhook secrets securely - Validate payload structure before processing ### Performance - Process webhooks asynchronously - Return 200 status quickly to avoid retries - Use queuing for heavy processing - Implement idempotency using delivery IDs ### Reliability - Handle duplicate deliveries gracefully - Log webhook events for debugging - Monitor webhook failures - Set up alerts for disabled webhooks ### Integration - Subscribe only to needed events - Filter events in your handler - Batch related operations when possible - Use webhook data to trigger workflows, not as primary data source ## Troubleshooting ### Common Issues **Webhook not triggered:** - Verify webhook is active in settings - Check event subscription includes the triggered event - Ensure HTTP API is enabled and running **Signature verification fails:** - Confirm secret matches webhook configuration - Check payload serialization (use exact JSON string) - Verify HMAC calculation implementation **Timeouts:** - Optimize endpoint response time - Return 200 status before heavy processing - Use async processing for complex operations **High failure count:** - Check endpoint availability - Verify URL and network connectivity - Review server logs for error details ### Debug Mode Enable debug logging by setting webhook events to verbose: ```javascript // In your webhook handler console.log('Headers:', req.headers); console.log('Body:', req.body); console.log('Signature verification:', isValidSignature); ``` ### Monitoring and Debugging The improved webhook interface provides better debugging tools: - **Visual status indicators** - Quickly identify inactive or failing webhooks - **Success/failure counts** - Monitor webhook health at a glance - **Card-based layout** - Easy scanning of multiple webhook configurations - **Transform file status** - Clear indication when payload transformations are active - **CORS warnings** - Visual alerts when custom headers are disabled ### Support For webhook-related issues: 1. Check webhook status indicators in the settings interface 2. Monitor success/failure counts for each webhook 3. Verify endpoint accessibility with the built-in test server 4. Test with webhook.site for quick validation 5. Review TaskNotes console logs for detailed error information 6. Use the included test server for local development and debugging