Automate Your Vault with Hooks and Webhooks

Set up automatic tagging, templating, and outbound webhook notifications triggered by document events.

intermediate17 min read

What You'll Build

In this guide you will wire up hooks and webhooks to automate repetitive tasks and integrate Lifestream Vault with external systems.

By the end you will have:

  • An auto-tag hook that adds tags to new documents via the document_operation action, scoped by path
  • An AI-prompt hook that summarizes new documents automatically via the ai_prompt action
  • An outbound webhook that notifies an external service when documents are created, updated, or deleted
  • A signature verification handler (Node.js) to validate incoming webhook payloads
  • Scripts to check delivery logs for your webhooks
Plan Required

Prerequisites

  • A Lifestream Vault account on the Pro tier or higher for hooks (document_operation, ai_prompt, webhook, etc.)
  • Pro tier or higher for outbound webhooks
  • Node.js 18+ for SDK usage and the example verification server
  • A publicly reachable HTTPS endpoint for your webhook receiver (for testing, smee.io or ngrok work well)

Hooks vs. Webhooks

Hooks and webhooks are related concepts but serve different purposes. Understanding the distinction will help you pick the right tool for each automation need.

Hooks are internal actions that run inside the Lifestream Vault platform in response to document events. They do not make outbound HTTP calls — they modify data or trigger behaviour within your vault.

Webhooks are outbound HTTP POST requests that Lifestream Vault sends to a URL you control. They let you integrate with external systems: Slack, Zapier, your own API, a CI/CD pipeline, etc.

FeatureHooksWebhooks
DirectionInternal (Vault → Vault)External (Vault → your server)
Action typeswebhook, ai_prompt, document_operation, auto_calendar_event, auto_booking_processHTTP POST to any URL
PayloadNo outbound payloadJSON event payload
Signature verificationN/AHMAC-SHA256 (X-Webhook-Signature header)
Retry logicN/A3 delivery attempts with exponential backoff
Minimum planProPro
Configured perVaultVault
Execution engineBullMQ worker (hook-executor)BullMQ worker (webhook-delivery)

Both hooks and webhooks respond to document events including document.created, document.updated, document.deleted, document.moved, and document.copied. A single event can trigger multiple hooks and multiple webhooks — they are independent.

Create an Auto-Tag Hook

An auto-tag hook inspects the path and frontmatter of a newly created document and applies tags automatically. This keeps your vault organised without requiring writers to remember to tag every document.

Example use case: any document created under meetings/ should automatically receive the meeting tag, and any document with the word "draft" in its frontmatter title should receive draft.

The config object passed to the hook defines the tagging rules. The available rule fields are:

FieldTypePurpose
pathPrefixstringApply tags when the document path starts with this value
tagsstring[]Tags to apply when the rule matches
titleContainsstringApply tags when the document title contains this substring
typescript
import { LifestreamVaultClient } from '@lifestreamdynamics/vault-sdk';

const { client } = await LifestreamVaultClient.login(
  'https://vault.lifestreamdynamics.com',
  'you@example.com',
  'your-password',
);

const VAULT_ID = 'vlt_abc123';

// hooks.create is positional: (vaultId, params).
// Tagging uses the 'document_operation' action with the 'add_tag' operation,
// scoped with an optional triggerFilter (e.g. a path pattern).
const meetingHook = await client.hooks.create(VAULT_ID, {
  name: 'Tag meeting notes',
  triggerEvent: 'document.created',
  triggerFilter: { pathPattern: 'meetings/**' },
  actionType: 'document_operation',
  actionConfig: { operation: 'add_tag', tag: 'meeting' },
});

console.log('Hook created:', meetingHook.id);

// A second hook can apply a different tag (each hook adds one tag)
const notesHook = await client.hooks.create(VAULT_ID, {
  name: 'Tag meeting notes (notes)',
  triggerEvent: 'document.created',
  triggerFilter: { pathPattern: 'meetings/**' },
  actionType: 'document_operation',
  actionConfig: { operation: 'add_tag', tag: 'notes' },
});

console.log('Notes hook created:', notesHook.id);

Tags applied by auto-tag hooks are merged with any tags already present in the document's frontmatter. Existing tags are never removed. You can apply the same hook to document.updated events as well if you want tags re-evaluated on every save.

Create an AI-Prompt Hook

Beyond tagging, the ai_prompt action type runs an AI pass over a newly created or updated document. The actionConfig.mode selects the pass — for example summarize generates a summary of the document's content.

Example use case: every document created under decisions/ is automatically summarized so the summary is available for dashboards and search previews.

The full set of action types is webhook, ai_prompt, document_operation, auto_calendar_event, and auto_booking_process. There is no template action type — use a document_operation (e.g. copy) or seed new documents from a template on the client side instead.

typescript
// hooks.create is positional: (vaultId, params).
// The ai_prompt action runs an AI pass; mode 'summarize' summarizes the doc.
const summarizeHook = await client.hooks.create(VAULT_ID, {
  name: 'Summarize decisions',
  triggerEvent: 'document.created',
  triggerFilter: { pathPattern: 'decisions/**' },
  actionType: 'ai_prompt',
  actionConfig: { mode: 'summarize' },
});

console.log('AI-prompt hook created:', summarizeHook.id);

Multiple hooks on the same event run in creation order (oldest first). If ordering matters — for example, you want a document_operation tag applied before an ai_prompt runs — create the tagging hook first.

Create a Webhook

A webhook sends an HTTP POST to a URL of your choosing whenever a document event fires. The request body is a JSON payload describing the event. A signature header lets your server verify the payload came from Lifestream Vault and was not tampered with.

Webhook payload structure:

{
  "id": "evt_abc123",
  "event": "document.created",
  "vaultId": "vlt_abc123",
  "documentPath": "posts/hello-world.md",
  "metadata": {},
  "timestamp": "2026-03-01T12:00:00.000Z"
}
typescript
// webhooks.create is positional: (vaultId, params).
// Only 'url' and 'events' are accepted — the HMAC signing secret is
// auto-generated by the server and returned ONCE on the creation response.
const webhook = await client.webhooks.create(VAULT_ID, {
  url: 'https://your-server.example.com/webhooks/vault',
  events: ['document.created', 'document.updated', 'document.deleted'],
});

console.log('Webhook created:', webhook.id);
console.log('Signing secret (store this now!):', webhook.secret);
// Store webhook.id — you will need it to check delivery logs

Webhook Event Types

Lifestream Vault supports the following webhook event types. You can subscribe a single webhook to any combination of these events, or use * to subscribe to all events.

EventTriggered when
document.createdA new document is saved for the first time
document.updatedAn existing document's content or metadata changes
document.deletedA document is deleted
document.movedA document is moved (renamed or relocated)
document.copiedA document is copied
directory.createdA new directory is created
document.overdueA document's due date has passed (checked every 15 min)
document.due-soonA document is approaching its due date

Notes on document.updated: This event fires on every content change, including auto-saves. For high-frequency editing workflows, consider debouncing your handler on the receiver side or subscribing only to specific events (e.g. document.created) to reduce noise.

Webhook deliveries are at-least-once — in rare cases of network failure during delivery, the same event may be delivered more than once. Design your webhook handler to be idempotent: use the id field in the payload as a deduplication key and skip processing if you have already handled that event ID.

Verifying Webhook Signatures

Every webhook delivery includes an X-Webhook-Signature header containing an HMAC-SHA256 digest of the raw request body, computed using the secret you provided when creating the webhook.

Header format:

X-Webhook-Signature: sha256=<hex-encoded-digest>

Verification steps:

  1. Read the raw request body as bytes (do not parse it as JSON first — JSON serialisation can change whitespace and key order)
  2. Compute HMAC-SHA256(secret, rawBody)
  3. Compare your digest with the value after sha256= in the header
  4. Use a constant-time comparison (e.g. crypto.timingSafeEqual) to prevent timing attacks
  5. Return 200 OK only if the signatures match; return 401 otherwise
typescript
import express from 'express';
import crypto from 'crypto';

const app = express();

// IMPORTANT: use express.raw() — NOT express.json() — so you get the raw bytes
app.post('/webhooks/vault', express.raw({ type: 'application/json' }), (req, res) => {
  const secret = process.env.LSV_WEBHOOK_SECRET!;
  const signature = req.headers['x-webhook-signature'] as string;

  if (!signature || !signature.startsWith('sha256=')) {
    return res.status(401).json({ error: 'Missing signature' });
  }

  const digest = crypto
    .createHmac('sha256', secret)
    .update(req.body) // req.body is a Buffer when using express.raw()
    .digest('hex');

  const expected = Buffer.from(`sha256=${digest}`);
  const received = Buffer.from(signature);

  // Constant-time comparison to prevent timing attacks
  if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature is valid — parse the event
  const event = JSON.parse(req.body.toString());
  console.log('Received event:', event.event, '| id:', event.id);

  // Handle the event
  switch (event.event) {
    case 'document.created':
      console.log('New document:', event.documentPath);
      break;
    case 'document.updated':
      console.log('Updated document:', event.documentPath);
      break;
    case 'document.deleted':
      console.log('Deleted document:', event.documentPath);
      break;
  }

  res.status(200).json({ received: true });
});

app.listen(3000, () => console.log('Webhook receiver listening on :3000'));

SSRF protection: Lifestream Vault blocks webhook delivery to private IP ranges (RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback addresses (127.0.0.1, ::1), and link-local addresses (169.254.0.0/16). Webhook URLs must resolve to a public IP. This prevents an attacker from using your webhook configuration to probe your internal network.

Checking Delivery Logs

Lifestream Vault logs every webhook delivery attempt. Each log entry records the HTTP status code your server returned, the response time, and whether the delivery was successful. Failed deliveries are retried automatically with exponential backoff — 3 total delivery attempts.

Retry schedule (approximate):

AttemptDelay after previous
2nd5 seconds
3rd30 seconds

After 3 failed attempts, the delivery is marked failed and no further retries are scheduled. (There is no manual-retry endpoint — re-trigger the underlying document event, or recreate the webhook, if you need a fresh attempt.)

typescript
const WEBHOOK_ID = 'wh_xyz789';

// listDeliveries is positional: (vaultId, webhookId) and returns
// WebhookDelivery[] directly (the most recent ~50 entries).
const deliveries = await client.webhooks.listDeliveries(VAULT_ID, WEBHOOK_ID);

for (const d of deliveries) {
  console.log(
    `HTTP ${d.statusCode ?? '—'} | attempt ${d.attempt} | ${d.deliveredAt ?? 'not delivered'}`,
  );
  if (d.error) console.log(`  Error: ${d.error}`);
}

// Identify failed deliveries (no successful delivery, error recorded).
// There is no retryDelivery method — re-trigger the source event if needed.
const failed = deliveries.filter((d) => !d.deliveredAt);
console.log(`${failed.length} deliveries did not succeed.`);

Tips & Best Practices

Always Verify Signatures

Never trust an incoming webhook payload without first verifying the X-Webhook-Signature header. Without verification, a malicious actor could send fake events to your endpoint and trigger unintended actions.

Make Your Handler Idempotent

Webhooks are delivered at-least-once. Use the id field in the event payload as a deduplication key. Before processing an event, check whether you have already handled that ID. Store processed IDs in Redis or a database with a short TTL (e.g. 24 hours).

Return 200 Quickly, Process Asynchronously

Lifestream Vault marks a delivery as failed if your server does not respond within 10 seconds. Return 200 OK immediately, then process the event in a background job (BullMQ, Inngest, a queue, etc.). This prevents timeout failures and retry storms.

Subscribe Only to Events You Need

If you are using webhooks to trigger external actions, subscribe to the specific event types your integration requires rather than * (all events). This reduces unnecessary traffic and avoids triggering handlers on events you don't care about. For example, a documentation pipeline might only need document.created and document.updated.

Hook Ordering Within a Vault

Multiple hooks on the same event fire in creation order (oldest first). If the order matters (e.g., you want to tag before templating), create the auto-tag hook before the template hook.

Test with the Delivery Logs

Before going to production, use the delivery logs to verify your endpoint is receiving and acknowledging events correctly. A common mistake is returning a non-2xx status code (e.g. a 301 redirect) which Lifestream Vault treats as a failure.

Treat the Webhook Secret Like an API Key

The signing secret is generated by the server and returned only once at creation time — store it securely. The secret is not updatable; if you need to rotate it, delete the webhook and create a new one, then update your receiver with the new secret.

What's Next

You have set up automatic tagging, templating, and outbound webhook notifications for your vault. Here are some places to explore next: