xiaohongshu
xiaohongshu
bknd-webhooks
Use when configuring webhook integrations in Bknd. Covers receiving incoming webhooks via HTTP triggers, sending outgoing webhooks with FetchTask, event-triggered webhooks on data changes, signature verification, retry patterns, and async processing.
Full skill instructions
Configure webhook integrations for receiving external events and sending notifications on data changes.
bknd-custom-endpoint)Webhook configuration requires code. No UI approach available.
| Type | Description | Approach |
|---|---|---|
| Incoming | Receive webhooks from external services | HTTP Trigger + Flow |
| Outgoing | Send webhooks when events occur | EventTrigger + FetchTask |
import { App, Flow, HttpTrigger, Task } from "bknd";
import { s } from "bknd/utils";
class WebhookReceiverTask extends Task<typeof WebhookReceiverTask.schema> {
override type = "webhook-receiver";
static override schema = s.strictObject({});
override async execute(input: Request) {
const body = await input.json();
const eventType = input.headers.get("x-event-type");
console.log(`Received webhook: ${eventType}`, body);
return { received: true, event: eventType };
}
}
const receiverTask = new WebhookReceiverTask("receive", {});
const webhookFlow = new Flow("incoming-webhook", [receiverTask]);
webhookFlow.setRespondingTask(receiverTask);
webhookFlow.setTrigger(
new HttpTrigger({
path: "/webhooks/external",
method: "POST",
mode: "async", // Return 200 immediately
})
);
const app = new App({
flows: { flows: [webhookFlow] },
});
import { createHmac, timingSafeEqual } from "crypto";
class SecureWebhookTask extends Task<typeof SecureWebhookTask.schema> {
override type = "secure-webhook";
static override schema = s.strictObject({
secret: s.string(),
});
override async execute(input: Request) {
const signature = input.headers.get("x-webhook-signature");
const body = await input.text();
// Verify signature
if (!this.verifySignature(body, signature)) {
throw this.error("Invalid signature", { signature });
}
const data = JSON.parse(body);
return { verified: true, data };
}
private verifySignature(payload: string, signature: string | null): boolean {
if (!signature) return false;
const expected = createHmac("sha256", this.params.secret)
.update(payload)
.digest("hex");
const sig = Buffer.from(signature);
const exp = Buffer.from(`sha256=${expected}`);
return sig.length === exp.length && timingSafeEqual(sig, exp);
}
}
const secureTask = new SecureWebhookTask("verify", {
secret: process.env.WEBHOOK_SECRET!,
});
const secureFlow = new Flow("secure-webhook", [secureTask]);
secureFlow.setRespondingTask(secureTask);
secureFlow.setTrigger(
new HttpTrigger({
path: "/webhooks/secure",
method: "POST",
})
);
import Stripe from "stripe";
class StripeWebhookTask extends Task<typeof StripeWebhookTask.schema> {
override type = "stripe-webhook";
static override schema = s.strictObject({
webhookSecret: s.string(),
});
override async execute(input: Request) {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const sig = input.headers.get("stripe-signature")!;
const body = await input.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
this.params.webhookSecret
);
} catch (err) {
throw this.error("Webhook verification failed", { err });
}
// Handle event types
switch (event.type) {
case "checkout.session.completed":
const session = event.data.object;
// Process successful payment...
break;
case "customer.subscription.deleted":
// Handle subscription cancellation...
break;
}
return { received: true, type: event.type };
}
}
class GitHubWebhookTask extends Task<typeof GitHubWebhookTask.schema> {
override type = "github-webhook";
static override schema = s.strictObject({
secret: s.string(),
});
override async execute(input: Request) {
const event = input.headers.get("x-github-event");
const delivery = input.headers.get("x-github-delivery");
const signature = input.headers.get("x-hub-signature-256");
const body = await input.text();
// Verify GitHub signature
const expected = createHmac("sha256", this.params.secret)
.update(body)
.digest("hex");
if (signature !== `sha256=${expected}`) {
throw this.error("Invalid GitHub signature");
}
const payload = JSON.parse(body);
switch (event) {
case "push":
console.log(`Push to ${payload.ref} by ${payload.pusher.name}`);
break;
case "pull_request":
console.log(`PR ${payload.action}: ${payload.pull_request.title}`);
break;
case "issues":
console.log(`Issue ${payload.action}: ${payload.issue.title}`);
break;
}
return { event, delivery };
}
}
For simpler cases, use plugin routes:
import { createPlugin } from "bknd";
import { Hono } from "hono";
const webhooksPlugin = createPlugin({
name: "webhooks",
onServerInit: (server) => {
const webhooks = new Hono();
// Stripe
webhooks.post("/stripe", async (c) => {
const sig = c.req.header("stripe-signature");
const body = await c.req.text();
// Verify and process...
return c.json({ received: true });
});
// GitHub
webhooks.post("/github", async (c) => {
const event = c.req.header("x-github-event");
const body = await c.req.json();
// Process...
return c.json({ received: true });
});
// Generic
webhooks.post("/:source", async (c) => {
const source = c.req.param("source");
const body = await c.req.json();
console.log(`Webhook from ${source}:`, body);
return c.json({ received: true });
});
server.route("/webhooks", webhooks);
},
});
Use Flows with EventTrigger to send webhooks when data changes.
import { App, Flow, FetchTask, EventTrigger } from "bknd";
// Task to send webhook
const sendWebhook = new FetchTask("send-webhook", {
url: "https://example.com/webhook",
method: "POST",
headers: [
{ key: "Content-Type", value: "application/json" },
{ key: "X-Webhook-Source", value: "my-app" },
],
body: "{{JSON.stringify(input)}}", // Forward event data
});
const webhookFlow = new Flow("outgoing-webhook", [sendWebhook]);
// Trigger on data event
webhookFlow.setTrigger(
new EventTrigger({
event: "mutator-insert-after", // After record created
mode: "async",
})
);
const app = new App({
flows: { flows: [webhookFlow] },
});
import { App, Flow, FetchTask, Task, EventTrigger } from "bknd";
import { s } from "bknd/utils";
// Filter task to check entity
class EntityFilterTask extends Task<typeof EntityFilterTask.schema> {
override type = "entity-filter";
static override schema = s.strictObject({
targetEntity: s.string(),
});
override async execute(input: any) {
if (input.entity?.name !== this.params.targetEntity) {
throw this.error("Skip - wrong entity");
}
return input;
}
}
const filterTask = new EntityFilterTask("filter", {
targetEntity: "orders",
});
const sendWebhook = new FetchTask("send", {
url: "https://api.example.com/orders/webhook",
method: "POST",
headers: [{ key: "Content-Type", value: "application/json" }],
body: "{{JSON.stringify({ event: 'order.created', data: input.changed })}}",
});
const flow = new Flow("order-webhook", [filterTask, sendWebhook]);
flow.task(filterTask).asInputFor(sendWebhook);
flow.setTrigger(
new EventTrigger({
event: "mutator-insert-after",
mode: "async",
})
);
import { Flow, FetchTask, EventTrigger, Condition } from "bknd";
// Send to multiple endpoints
const sendSlack = new FetchTask("slack", {
url: "https://hooks.slack.com/services/XXX/YYY/ZZZ",
method: "POST",
headers: [{ key: "Content-Type", value: "application/json" }],
body: '{"text": "New order: {{input.changed.id}}"}',
});
const sendDiscord = new FetchTask("discord", {
url: "https://discord.com/api/webhooks/XXX/YYY",
method: "POST",
headers: [{ key: "Content-Type", value: "application/json" }],
body: '{"content": "New order: {{input.changed.id}}"}',
});
const sendCustom = new FetchTask("custom", {
url: process.env.CUSTOM_WEBHOOK_URL!,
method: "POST",
headers: [{ key: "Content-Type", value: "application/json" }],
body: "{{JSON.stringify(input)}}",
});
const flow = new Flow("multi-webhook", [sendSlack, sendDiscord, sendCustom]);
// All tasks run in parallel (no connections)
flow.setTrigger(
new EventTrigger({
event: "mutator-insert-after",
mode: "async",
})
);
import { Flow, FetchTask, Task, Condition, EventTrigger } from "bknd";
import { s } from "bknd/utils";
class RetryTask extends Task<typeof RetryTask.schema> {
override type = "retry-webhook";
static override schema = s.strictObject({
url: s.string(),
maxRetries: s.number({ default: 3 }),
delayMs: s.number({ default: 1000 }),
});
override async execute(input: any) {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= this.params.maxRetries; attempt++) {
try {
const response = await fetch(this.params.url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (response.ok) {
return { success: true, attempt };
}
lastError = new Error(`HTTP ${response.status}`);
} catch (err) {
lastError = err as Error;
}
// Wait before retry (exponential backoff)
if (attempt < this.params.maxRetries) {
await new Promise((r) => setTimeout(r, this.params.delayMs * attempt));
}
}
throw this.error("All retries failed", { lastError: lastError?.message });
}
}
Bknd emits these events that can trigger webhooks:
| Event Slug | Description | Payload |
|---|---|---|
mutator-insert-before | Before record created | { entity, data } |
mutator-insert-after | After record created | { entity, data, changed } |
mutator-update-before | Before record updated | { entity, entityId, data } |
mutator-update-after | After record updated | { entity, entityId, data, changed } |
mutator-delete-before | Before record deleted | { entity, entityId } |
mutator-delete-after | After record deleted | { entity, entityId, data } |
| Event Slug | Description | Payload |
|---|---|---|
file-uploaded | File uploaded | { name, meta, etag, file, state } |
file-deleted | File deleted | { name } |
file-access | File accessed | { name } |
// Event payload structure for mutator-insert-after
interface InsertAfterPayload {
entity: {
name: string; // Entity name, e.g., "orders"
fields: Field[]; // Entity fields
};
data: Record<string, any>; // Original input data
changed: Record<string, any>; // Resulting record with ID
}
class ProcessEventTask extends Task {
override async execute(input: InsertAfterPayload) {
const entityName = input.entity.name;
const recordId = input.changed.id;
const recordData = input.changed;
// Send webhook with structured data
await fetch("https://api.example.com/webhook", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event: `${entityName}.created`,
timestamp: new Date().toISOString(),
data: recordData,
}),
});
return { sent: true };
}
}
import { App, em, entity, text, number, Flow, FetchTask, Task, EventTrigger, Condition } from "bknd";
import { s } from "bknd/utils";
// Schema
const schema = em({
orders: entity({
customer_email: text().required(),
total: number().required(),
status: text().default("pending"),
}),
});
// Filter for orders only
class OrderFilterTask extends Task<typeof OrderFilterTask.schema> {
override type = "order-filter";
static override schema = s.strictObject({});
override async execute(input: any) {
if (input.entity?.name !== "orders") {
throw this.error("Not an order");
}
return input.changed; // Pass order data
}
}
// Format webhook payload
class FormatWebhookTask extends Task<typeof FormatWebhookTask.schema> {
override type = "format-webhook";
static override schema = s.strictObject({});
override async execute(order: any) {
return {
event: "order.created",
timestamp: new Date().toISOString(),
order: {
id: order.id,
email: order.customer_email,
total: order.total,
status: order.status,
},
};
}
}
const filterTask = new OrderFilterTask("filter", {});
const formatTask = new FormatWebhookTask("format", {});
// Send to multiple destinations
const sendSlack = new FetchTask("slack", {
url: process.env.SLACK_WEBHOOK_URL!,
method: "POST",
headers: [{ key: "Content-Type", value: "application/json" }],
body: '{"text": "New order #{{input.order.id}} - ${{input.order.total}}"}',
});
const sendExternal = new FetchTask("external", {
url: process.env.EXTERNAL_WEBHOOK_URL!,
method: "POST",
headers: [
{ key: "Content-Type", value: "application/json" },
{ key: "X-API-Key", value: process.env.EXTERNAL_API_KEY! },
],
body: "{{JSON.stringify(input)}}",
});
// Build flow
const orderWebhookFlow = new Flow("order-notifications", [
filterTask,
formatTask,
sendSlack,
sendExternal,
]);
// Connect: filter -> format -> [slack, external] (parallel)
orderWebhookFlow.task(filterTask).asInputFor(formatTask);
orderWebhookFlow.task(formatTask).asInputFor(sendSlack);
orderWebhookFlow.task(formatTask).asInputFor(sendExternal);
// Trigger on new orders
orderWebhookFlow.setTrigger(
new EventTrigger({
event: "mutator-insert-after",
mode: "async",
})
);
const app = new App({
data: { schema },
flows: { flows: [orderWebhookFlow] },
});
# Basic test
curl -X POST http://localhost:7654/webhooks/external \
-H "Content-Type: application/json" \
-H "X-Event-Type: test" \
-d '{"test": true}'
# With signature (HMAC-SHA256)
PAYLOAD='{"test":true}'
SECRET="your-secret"
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
curl -X POST http://localhost:7654/webhooks/secure \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=$SIGNATURE" \
-d "$PAYLOAD"
Use webhook.site or similar:
// Temporarily point to test URL
const sendWebhook = new FetchTask("send", {
url: "https://webhook.site/your-unique-id",
method: "POST",
body: "{{JSON.stringify(input)}}",
});
Then create a record:
curl -X POST http://localhost:7654/api/data/orders \
-H "Content-Type: application/json" \
-d '{"customer_email": "[email protected]", "total": 99.99}'
Problem: Incoming webhook returns 200 but doesn't process
Fix: Check mode - async returns immediately:
// Async mode processes in background
new HttpTrigger({ mode: "async" });
// For debugging, use sync
new HttpTrigger({ mode: "sync" });
Problem: Valid webhooks rejected
Fix: Ensure you're reading raw body before parsing:
// WRONG - body already parsed
const body = await input.json();
const sig = verify(JSON.stringify(body), signature);
// CORRECT - read raw text first
const bodyText = await input.text();
const verified = verify(bodyText, signature);
const body = JSON.parse(bodyText);
Problem: EventTrigger flow doesn't run
Fix: Check event name matches exactly:
// Available events (use exact slugs)
"mutator-insert-after" // Not "data:entity:created"
"mutator-update-after" // Not "data:entity:updated"
"mutator-delete-after" // Not "data:entity:deleted"
Problem: Webhook fires for every entity, not just target
Fix: Add entity filter task:
class EntityFilter extends Task {
async execute(input) {
if (input.entity?.name !== "orders") {
throw this.error("Skip"); // Stops flow
}
return input;
}
}
Problem: {{input}} appears literally in body
Fix: Use proper template syntax:
// WRONG
body: "{ data: {{input}} }"
// CORRECT
body: "{{JSON.stringify({ data: input })}}"
DO:
mode: "async" for incoming webhooks (return 200 fast)DON'T:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience