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.
- Enable HTTP API in TaskNotes Settings → Integrations → HTTP API
- 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
TaskNotes provides a modern, intuitive interface for managing webhooks:
- 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
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
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
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
TaskNotes triggers webhooks for the following 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.started- When time tracking starts on a tasktime.stopped- When time tracking stops on a task
pomodoro.started- When a pomodoro session beginspomodoro.completed- When a pomodoro session finishes successfullypomodoro.interrupted- When a pomodoro session is interrupted
recurring.instance.completed- When a recurring task instance is marked complete
reminder.triggered- When a task reminder fires and displays a notification
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": "task.created",
"data": {
"task": { /* TaskInfo object */ }
}
}{
"event": "task.updated",
"data": {
"task": { /* Current TaskInfo */ },
"previous": { /* Previous TaskInfo */ }
}
}{
"event": "time.started",
"data": {
"task": { /* TaskInfo object */ },
"session": {
"startTime": "2024-03-15T14:30:00.000Z",
"endTime": null,
"description": null
}
}
}{
"event": "task.created",
"data": {
"task": { /* TaskInfo object */ },
"source": "nlp",
"originalText": "Review PR #123 tomorrow high priority @work"
}
}{
"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"
}
}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'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');
});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');
});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');
});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');
});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');
});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"]
}'curl http://localhost:8080/api/webhooks \
-H "Authorization: Bearer YOUR_API_TOKEN"curl -X DELETE http://localhost:8080/api/webhooks/WEBHOOK_ID \
-H "Authorization: Bearer YOUR_API_TOKEN"curl http://localhost:8080/api/webhooks/deliveries \
-H "Authorization: Bearer YOUR_API_TOKEN" - Failed deliveries retry 3 times with exponential backoff (1s, 2s, 4s)
- Webhooks automatically disabled after 10+ consecutive failures
- Each delivery gets a unique ID for tracking
- Success/failure counts maintained per webhook
- Recent delivery history available via API
- 10-second timeout per delivery attempt
- HTTP status codes tracked for debugging
- Failed webhooks don't block TaskNotes operations
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!
- 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 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 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}"
}
}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;
}- 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
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:
- Start with a simple transform that returns the original payload
- Add small changes incrementally
- Use webhook.site or the built-in test server to inspect outputs
- Check TaskNotes console for transformation errors
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.
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 8080The 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
Use the test secret when adding the webhook:
URL: http://localhost:3000/webhook
Secret: test-secret-key-for-tasknotes-webhooks
# Install ngrok
npm install -g ngrok
# Expose local server
ngrok http 3000
# Use the ngrok URL in webhook settings
# https://abc123.ngrok.io/webhookUse 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
- Always verify webhook signatures
- Use HTTPS endpoints in production
- Store webhook secrets securely
- Validate payload structure before processing
- Process webhooks asynchronously
- Return 200 status quickly to avoid retries
- Use queuing for heavy processing
- Implement idempotency using delivery IDs
- Handle duplicate deliveries gracefully
- Log webhook events for debugging
- Monitor webhook failures
- Set up alerts for disabled webhooks
- 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
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
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);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
For webhook-related issues:
- Check webhook status indicators in the settings interface
- Monitor success/failure counts for each webhook
- Verify endpoint accessibility with the built-in test server
- Test with webhook.site for quick validation
- Review TaskNotes console logs for detailed error information
- Use the included test server for local development and debugging