This walkthrough builds a NestJS app that:
contact.created trigger when a new contact is saved.createContact action that Bondi can call from a workflow, verified by HMAC.End-to-end time: ~10 minutes.
nest new my-crm to scaffold one)npm install @bondi-labs/integration-sdk reflect-metadata zod
npx bondi init
This opens your browser, prompts you to approve the CLI, asks for the integration name (e.g. "My CRM"), and writes the result to .env:
BONDI_API_URL=https://automation.heybondi.com
BONDI_WORKSPACE_ID=ws-...
BONDI_INTEGRATION_TOKEN=bnd_tok_...
Save the token now. It's shown once; rotation is possible later but the previous token is only valid for 24h after rotation.
Triggers live as injectable classes that extend BondiTriggerBase<TZodSchema>. The Zod schema gives you full type safety on emit().
// src/bondi/triggers/contact-created.trigger.ts
import { BondiTrigger, BondiTriggerBase } from "@bondi-labs/integration-sdk/nestjs";
import { z } from "zod";
export const contactCreatedPayload = z.object({
id: z.string(),
email: z.string().email(),
fullName: z.string(),
});
@BondiTrigger({
name: "contact.created",
label: "Contact Created",
payload: contactCreatedPayload,
})
export class ContactCreatedTrigger extends BondiTriggerBase<typeof contactCreatedPayload> {}
// src/bondi/bondi.module.ts
import { Module } from "@nestjs/common";
import { BondiModule } from "@bondi-labs/integration-sdk/nestjs";
import { ContactCreatedTrigger } from "./triggers/contact-created.trigger";
@Module({
imports: [
BondiModule.forRoot({
integration: { name: "My CRM", slug: "my-crm", category: "crm" },
triggers: [ContactCreatedTrigger],
}),
],
exports: [BondiModule],
})
export class AppBondiModule {}
Import AppBondiModule from AppModule. The module is global, so any service can inject any registered trigger.
// src/contacts/contacts.service.ts
import { Injectable } from "@nestjs/common";
import { ContactCreatedTrigger } from "../bondi/triggers/contact-created.trigger";
@Injectable()
export class ContactsService {
constructor(private contactCreated: ContactCreatedTrigger) {}
async create(dto: { email: string; fullName: string }) {
const contact = await saveToDb(dto);
this.contactCreated.emit({
id: contact.id,
email: contact.email,
fullName: contact.fullName,
});
// ^ Type-checked against contactCreatedPayload's Zod schema.
return contact;
}
}
emit() is fire-and-forget — failures don't bubble into your business logic. Use emitAndWait() if you need to confirm Bondi accepted the event before returning.
// src/contacts/contacts.controller.ts
import { Body, Controller, Post, UseGuards } from "@nestjs/common";
import {
BondiAction,
BondiGuard,
BondiService,
} from "@bondi-labs/integration-sdk/nestjs";
import { z } from "zod";
import { ContactsService } from "./contacts.service";
const createContactBody = z.object({
email: z.string().email(),
fullName: z.string(),
});
@BondiService({ name: "contacts", label: "Contacts" })
@Controller("contacts")
export class ContactsController {
constructor(private contacts: ContactsService) {}
@UseGuards(BondiGuard)
@BondiAction({
name: "createContact",
label: "Create Contact",
body: createContactBody,
})
@Post()
async create(@Body() dto: z.infer<typeof createContactBody>) {
return this.contacts.create(dto);
}
}
BondiGuard verifies the HMAC signature against the raw bytes of the request body. NestJS must be configured to preserve them:
// src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule, { rawBody: true });
await app.listen(3000);
}
bootstrap();
Without rawBody: true, all BondiGuard checks will fail with missing_headers.
After defining triggers and actions, push the schema to Bondi so the workflow editor knows what fields exist:
npx bondi sync
By default this imports ./src/bondi.integration.ts — a file that default-exports the integration definition:
// src/bondi.integration.ts
import { defineIntegration } from "@bondi-labs/integration-sdk/core";
import { contactCreatedPayload } from "./bondi/triggers/contact-created.trigger";
export default defineIntegration({
name: "My CRM",
slug: "my-crm",
category: "crm",
baseUrl: "https://api.mycrm.com/v1",
services: [
{
name: "contacts",
label: "Contacts",
triggers: [
{ name: "contact.created", label: "Contact Created", payload: contactCreatedPayload },
],
actions: [
// ... define each action with its Zod schemas
],
},
],
});
Use --entry to point to a different file, --dry-run to preview, or --fail-on-error for CI.
BondiGuard