Set up automatic tagging, templating, and outbound webhook notifications triggered by document events.
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:
document_operation action, scoped by pathai_prompt actionPrerequisites
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.
| Feature | Hooks | Webhooks |
|---|---|---|
| Direction | Internal (Vault → Vault) | External (Vault → your server) |
| Action types | webhook, ai_prompt, document_operation, auto_calendar_event, auto_booking_process | HTTP POST to any URL |
| Payload | No outbound payload | JSON event payload |
| Signature verification | N/A | HMAC-SHA256 (X-Webhook-Signature header) |
| Retry logic | N/A | 3 delivery attempts with exponential backoff |
| Minimum plan | Pro | Pro |
| Configured per | Vault | Vault |
| Execution engine | BullMQ 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.
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:
| Field | Type | Purpose |
|---|---|---|
pathPrefix | string | Apply tags when the document path starts with this value |
tags | string[] | Tags to apply when the rule matches |
titleContains | string | Apply tags when the document title contains this substring |
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.
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, andauto_booking_process. There is notemplateaction type — use adocument_operation(e.g.copy) or seed new documents from a template on the client side instead.
// 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.
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"
}
// 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 logsLifestream 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.
| Event | Triggered when |
|---|---|
document.created | A new document is saved for the first time |
document.updated | An existing document's content or metadata changes |
document.deleted | A document is deleted |
document.moved | A document is moved (renamed or relocated) |
document.copied | A document is copied |
directory.created | A new directory is created |
document.overdue | A document's due date has passed (checked every 15 min) |
document.due-soon | A 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.
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:
HMAC-SHA256(secret, rawBody)sha256= in the headercrypto.timingSafeEqual) to prevent timing attacks200 OK only if the signatures match; return 401 otherwiseimport 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.
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):
| Attempt | Delay after previous |
|---|---|
| 2nd | 5 seconds |
| 3rd | 30 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.)
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.`);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.
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).
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.
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.
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.
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.
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.
You have set up automatic tagging, templating, and outbound webhook notifications for your vault. Here are some places to explore next:
client.hooks: list, update, delete, and reorder hooksclient.webhooks: create, list, update, delete, list deliveriesenableRequestSigning on the SDK client to sign mutating API requests with HMAC-SHA256