| name | eventing |
| summary | Couchbase Eventing Service โ write, deploy, and troubleshoot Eventing Functions that react to document mutations in real time |
| description | Couchbase Eventing Service โ write, deploy, and troubleshoot Eventing Functions that react to document mutations in real time |
| compatibility | Requires Couchbase Server 6.5+ with Eventing Service enabled. Functions are JavaScript (ES5 subset via V8). |
| metadata | {"last_verified":"2026-05","min_server_version":"6.5","handoff":[{"condition":"user asks about SQL++ queries","type":"variant","skill":"server-querying-python"},{"condition":"user asks about streaming to Kafka","skill":"kafka"},{"condition":"user asks about Couchbase fundamentals or core concepts","skill":"getting-started"},{"condition":"user asks about atomic multi-document operations or whether to use Eventing vs Transactions","type":"variant","skill":"transactions-python"},{"condition":"user asks about full-text search or vector search from Eventing Functions","type":"variant","skill":"search-python"}]} |
Couchbase Eventing
JavaScript functions that trigger on document mutations โ create, update, delete, and expiry.
Template: templates/eventing-function.js โ copy this skeleton as a starting point for new Eventing Functions.
Function Structure
function OnUpdate(doc, meta) {
if (doc.type !== "order") return;
dst[meta.id] = Object.assign({}, doc, { processed: true });
}
function OnDelete(meta, options) {
delete dst[meta.id];
}
Bucket Bindings
Bind collections to global variables in function settings. Bound collections act as maps:
dst[meta.id] = { ...doc, enriched: true };
var existing = dst[meta.id];
delete dst["stale_" + meta.id];
Alias modes: read-only (GET only) or read+write (GET, SET, DELETE).
Inline SQL++
var customerId = doc.customerId;
var rows = SELECT name, email FROM `myapp`._default.customers WHERE id = $customerId;
for (var row of rows) {
dst[meta.id] = Object.assign({}, doc, { customerName: row.name });
break;
}
rows.close();
Rules: use $varName for substitution (assign meta.id to a local var first); always call rows.close(); use /* */ not // in multi-line statements; DML cannot target the source collection.
Common Patterns
Data enrichment
function OnUpdate(doc, meta) {
if (doc.enriched) return;
var pid = doc.productId;
var rows = SELECT name, category FROM `myapp`.catalog.products WHERE META().id = $pid;
for (var p of rows) { dst[meta.id] = Object.assign({}, doc, { productName: p.name, enriched: true }); break; }
rows.close();
}
Cascade delete
function OnDelete(meta, options) {
var userId = meta.id;
var rows = SELECT META().id AS oid FROM `myapp`._default.orders WHERE userId = $userId;
for (var row of rows) { delete orders[row.oid]; }
rows.close();
}
Timers (scheduled execution)
function OnUpdate(doc, meta) {
if (doc.type !== "subscription") return;
createTimer(onExpiry, new Date(doc.expiresAt), meta.id, { userId: doc.userId });
}
function onExpiry(ctx) {
notifications[ctx.userId] = { type: "expired", at: new Date().toISOString() };
}
function OnDelete(meta, options) {
cancelTimer(onExpiry, meta.id);
}
createTimer(callback, fireAt, reference, context) โ reference is unique per function+callback; calling again with the same reference replaces the timer. Timers fire at-least-once โ make callbacks idempotent.
External webhook (curl)
function OnUpdate(doc, meta) {
if (doc.status !== "shipped") return;
var r = curl("POST", "https://hooks.example.com/shipped", {
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + TOKEN },
body: JSON.stringify({ orderId: meta.id }),
timeout: 5000
});
if (r.status !== 200) log("Webhook failed:", r.status);
}
curl(method, url, {headers, body, timeout, encoding}) โ {status, headers, body}. Use Constant bindings for secrets โ never hardcode tokens.
Advanced Keyspace Accessors
Use when bindings use wildcard scope/collection, or when you need CAS/expiry:
var result = couchbase.get(src, meta);
if (result.success) log("CAS:", result.meta.cas);
couchbase.upsert(dst, { id: meta.id }, doc);
couchbase.replace(dst, { id: meta.id, cas: result.meta.cas }, doc);
couchbase.insert(dst, { id: "new_key" }, { field: "value" });
couchbase.delete(dst, { id: meta.id });
Meta argument: id (required), cas (optimistic locking), expiry (TTL seconds).
couchbase.query() โ Explicit N1QL API (Server 7.2+)
couchbase.query() is the preferred way to run parameterized SQL++ queries from Eventing when you need explicit control over query options (consistency, timeout, named parameters). It is cleaner than inline SQL++ for complex or reusable queries.
function OnUpdate(doc, meta) {
if (doc.type !== "order") return;
var customerId = doc.customerId;
var result = couchbase.query(
"SELECT name, email, tier FROM `myapp`._default.customers WHERE META().id = $cid",
{ namedParams: { cid: customerId }, consistency: "request_plus" }
);
if (result.success) {
var rows = result.results;
if (rows.length > 0) {
dst[meta.id] = Object.assign({}, doc, {
customerName: rows[0].name,
customerTier: rows[0].tier,
enriched: true
});
}
} else {
log("query failed:", result.error);
}
}
Options object fields:
namedParams โ object of $name: value substitutions
positionalParams โ array for $1, $2 positional parameters
consistency โ "none" (default, fastest) or "request_plus" (read-your-writes)
timeout โ milliseconds (default: function timeout)
Return value: { success: bool, results: [...rows], error: string }
vs inline SQL++:
- Inline SQL++ (
var rows = SELECT ...) is simpler for one-off queries but has no error handling and limited option control
couchbase.query() returns a result object you can inspect for errors and is easier to test
- Both are synchronous โ neither supports
async/await
couchbase.analyticsQuery()
var rows = couchbase.analyticsQuery(
"SELECT * FROM `myapp`._default.orders WHERE status = $status LIMIT $limit;",
{ status: doc.filterStatus, limit: 10 }
);
var results = [];
for (var row of rows) results.push(row);
dst[meta.id] = Object.assign({}, doc, { results: results });
Use instead of inline SQL++ for heavy aggregations or Analytics dataset queries.
Suppressing Double Mutations
When a function writes to a collection it also reads from, use crc64 to skip unchanged documents:
function OnUpdate(doc, meta) {
if (meta.id.startsWith("_sync")) return;
var curr = crc64(doc);
if (checksums[meta.id] === curr) return;
checksums[meta.id] = curr;
dst[meta.id] = processDoc(doc);
}
Language Constraints
| Supported | Not Supported |
|---|
| ES5 JavaScript (V8) | Global variables (no state between calls) |
Inline SQL++, curl(), Timers | async/await, Promises |
log(), crc64() | setTimeout, setInterval, require() |
No global state โ persist all state to a bound collection.
Deployment
Feed boundary controls which mutations trigger the function:
Everything โ existing documents + new mutations (backfill)
From now โ only new mutations after deployment
curl -X POST http://localhost:8096/api/v1/functions/my-function/deploy -u Administrator:"$CB_ADMIN_PASSWORD"
curl -X POST http://localhost:8096/api/v1/functions/my-function/undeploy -u Administrator:"$CB_ADMIN_PASSWORD"
Troubleshooting
Not triggering: verify source collection and that the function is deployed (not just saved). From now won't process existing documents.
Resource exhaustion: missing rows.close(). Always close result sets, even in error paths.
Double mutations: use crc64 checksums or a processed flag.
Slow function: inline SQL++ without an index causes full scans per mutation. Add a GSI.
Logs: Eventing UI โ function โ Logs, or GET http://localhost:8096/api/v1/functions/my-function/applog.