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¶
- Enable HTTP API in TaskNotes Settings → HTTP API tab
- Add a webhook by clicking "Add Webhook" in the webhook settings
- Select events you want to receive notifications for
- Configure transformation (optional) for custom payload formats
- 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 createdtask.updated
- When a task is modifiedtask.deleted
- When a task is removedtask.completed
- When a task status changes to completedtask.archived
- When a task is archivedtask.unarchived
- When a task is unarchived
Time Tracking Events¶
time.started
- When time tracking starts on a tasktime.stopped
- When time tracking stops on a task
Pomodoro Events¶
pomodoro.started
- When a pomodoro session beginspomodoro.completed
- When a pomodoro session finishes successfullypomodoro.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:
{
"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¶
{
"event": "task.created",
"data": {
"task": { /* TaskInfo object */ }
}
}
Task Updated (includes previous state)¶
{
"event": "task.updated",
"data": {
"task": { /* Current TaskInfo */ },
"previous": { /* Previous TaskInfo */ }
}
}
Time Tracking Events¶
{
"event": "time.started",
"data": {
"task": { /* TaskInfo object */ },
"session": {
"startTime": "2024-03-15T14:30:00.000Z",
"endTime": null,
"description": null
}
}
}
NLP Task Creation¶
{
"event": "task.created",
"data": {
"task": { /* TaskInfo object */ },
"source": "nlp",
"originalText": "Review PR #123 tomorrow high priority @work"
}
}
Reminder Events¶
{
"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):
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):
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¶
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¶
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¶
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¶
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¶
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)¶
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¶
curl http://localhost:8080/api/webhooks \
-H "Authorization: Bearer YOUR_API_TOKEN"
Delete Webhook¶
curl -X DELETE http://localhost:8080/api/webhooks/WEBHOOK_ID \
-H "Authorization: Bearer YOUR_API_TOKEN"
View Delivery History¶
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 - Ready-to-use transform files for Discord, Slack, Teams, and more!
How Transform Files Work¶
- File Location: Transform files must be stored in your Obsidian vault
- File Types: Supports
.js
(JavaScript) and.json
(JSON template) files - Execution: Files are read and executed when a webhook is triggered
- Safety: JavaScript files run in a controlled context for security
- 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:
function transform(payload) {
// Your transformation logic here
return transformedPayload;
}
Complete Discord Example:
// 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:
// 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:
{
"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:
{
"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:
{
"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:
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:
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:
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:
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 typeX-TaskNotes-Signature
: HMAC signatureX-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:
# 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¶
# 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 for quick testing:
- Go to webhook.site and copy your unique URL
- Add it as a webhook in TaskNotes
- Perform actions to trigger events
- 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:
// 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