Trigger emits are async network calls. This guide covers how the SDK handles failures, retries, and offline development.
emit() vs emitAndWait()Two flavors:
| Method | Returns | Throws? | Use when |
|---|---|---|---|
emit(payload) |
void |
Never | Best-effort observability — don't block business logic |
emitAndWait(payload) |
Promise<void> |
On any failure | You need to confirm Bondi accepted the event before proceeding |
// Fire and forget — most common case
this.contactCreated.emit({ id, email, fullName });
// Awaiting — for transactional flows
await this.contactCreated.emitAndWait({ id, email, fullName });
Default recommendation: use emit(). Webhook delivery has its own retry queue inside Bondi; failing to enqueue should not break user-facing requests.
emit()emit() swallows errors so they never bubble into your business code. Configure a callback to log/observe them:
import { createBondiClient } from "@bondi-labs/integration-sdk/core";
const client = createBondiClient({
workspaceId: process.env.BONDI_WORKSPACE_ID,
token: process.env.BONDI_INTEGRATION_TOKEN,
apiUrl: process.env.BONDI_API_URL,
onEmitError: (err, eventName) => {
logger.warn({ err, eventName }, "Bondi emit failed");
sentryClient.captureException(err, { tags: { eventName } });
},
});
For NestJS, use BondiClient.config.onEmitError after binding (less common — usually you set it via a factory in BondiModule.forRoot):
BondiModule.forRoot({
integration: { name: "My CRM", slug: "my-crm", category: "crm" },
triggers: [...],
// onEmitError: not directly supported in forRoot, but you can wrap BondiClient yourself
})
If you need custom error handling in NestJS, consider using
emitAndWait()inside a try/catch — that gives you full control.
The internal client retries network failures and 5xx responses up to 3 times with exponential backoff (1s, 2s, 4s). 4xx responses are NOT retried — they're treated as permanent errors (auth failures, validation rejections).
Retry is opaque to your code: by the time emit() calls onEmitError (or emitAndWait() resolves/rejects), retries are exhausted.
The retry config is currently not customer-configurable. If you need different behavior, file an issue.
For local development without a real Bondi connection:
const client = createBondiClient({
workspaceId: process.env.BONDI_WORKSPACE_ID,
token: process.env.BONDI_INTEGRATION_TOKEN,
apiUrl: process.env.BONDI_API_URL,
dryRun: true, // explicit
});
In dry-run mode, emit() logs the payload to console instead of making an HTTP call:
[Bondi DryRun] contact.created { id: "1", email: "alice@example.com" }
Auto dry-run: if token, apiUrl, or workspaceId is missing/empty, the client switches to dry-run automatically. This is the recommended pattern for local development — just don't set the env vars on your dev machine and emits become no-ops with logs.
| Symptom | Probable cause | Fix |
|---|---|---|
401 invalid_signature on Bondi side |
Token mismatch or stale token after rotation | Re-run npx bondi init or update BONDI_INTEGRATION_TOKEN from Studio rotation |
401 expired_timestamp |
Clock drift (>5 min) | Run NTP; ensure server time is correct |
| 404 on emit | Slug mismatch — integration deleted or never synced | npx bondi sync to register the definition |
410 Integration is deactivated |
Soft-deleted from Studio | Re-create the integration |
| 429 | Rate-limited (60/min per integration, 1000/hr per workspace) | Throttle emits; consider batching at app level |
BondiClient not injected (NestJS) |
BondiModule.forRoot() not imported, or trigger class missing from triggers: [...] |
Add the trigger class to the module options |
onEmitError configured to ship errors to your observability stackBONDI_INTEGRATION_TOKEN stored as a secret (Vault/Secrets Manager/etc.) — not committeddryRun: true)npx bondi sync --fail-on-error --dry-run to catch broken definitions before deploy