Use when you have a written implementation plan to execute in a separate session with review checkpoints
npx skills add bkper/skills --skill "Event handling (events handler)"
Install specific skill from multi-skill repository
# Description
Does something useful
# SKILL.md
Bkper App Development
Core knowledge for building Bkper apps using bkper-js SDK and Workers for Platforms.
Development Workflow
Web Handler (UI)
The web handler provides the UI shown when users open your app from Bkper's menu. Development uses a local server with hot-reload:
bun run dev
Changes to frontend code reflect immediately. The local URL is configured in bkperapp.yaml as menuUrlDev.
Events Handler (Webhooks)
The events handler processes webhooks from Bkper. Development uses the dev environment instead of local:
bun run deploy:dev
This deploys to https://{app-id}-dev.bkper.app/events — a stable URL configured as webhookUrlDev in bkperapp.yaml.
Why deploy instead of local?
- Webhook testing involves triggering events from Bkper
- The dev environment tests against real platform resources (KV, secrets)
- No tunnel setup required
Continuous Development Pattern
When iterating on the events handler:
- Watch
packages/events/src/for file changes - On change: run
bun run deploy:dev - Report deployment status to developer
This pattern enables rapid iteration. AI coding agents can automate this cycle.
Production Deployment
bun run deploy
Builds all packages and deploys to https://{app-id}.bkper.app.
Bkper Core Concepts
Data Model
- Book: A ledger that tracks resources (money, inventory, etc.) with accounts and transactions
- Account: A place where resources accumulate (e.g., Bank, Revenue, Inventory)
- Transaction: A movement of resources between two accounts (always debit + credit)
- Group: A way to organize accounts (e.g., "Operating Expenses")
Account Types
| Type | Nature | Balance Behavior |
|---|---|---|
ASSET |
Permanent | Increases with debits |
LIABILITY |
Permanent | Increases with credits |
INCOMING |
Temporary | Increases with credits |
OUTGOING |
Temporary | Increases with debits |
The Zero-Sum Invariant
Every transaction moves an amount FROM one account TO another. The sum of all account balances in a book always equals zero. This is fundamental — never break this invariant.
bkper-js SDK
Installation
bun add bkper-js
Initialization (Cloudflare Workers)
import { Bkper, Book } from "bkper-js";
// Per-request Bkper instance (recommended for Workers)
function createBkper(env: Env, oauthToken?: string): Bkper {
return new Bkper({
oauthTokenProvider: async () => oauthToken,
agentIdProvider: async () => env.BKPER_AGENT_ID,
});
}
Book Operations
// Get a book
const book = await bkper.getBook(bookId);
// With accounts pre-loaded
const book = await bkper.getBook(bookId, true);
// Book properties
book.getName();
book.getDatePattern(); // dd/MM/yyyy | MM/dd/yyyy | yyyy/MM/dd
book.getFractionDigits(); // decimal places (0-8)
book.formatDate(new Date());
book.parseDate("25/01/2024");
book.formatValue(1234.56);
// Get accounts
const accounts = await book.getAccounts();
const account = await book.getAccount("Account Name");
Transaction Operations
import { Transaction } from "bkper-js";
// Create and post a transaction
const tx = new Transaction(book)
.setDate("2024-01-25")
.setAmount(100.5)
.from(creditAccount)
.to(debitAccount)
.setDescription("Payment #invoice123")
.setProperty("external_id", "123")
.addRemoteId("external-system-id");
await tx.create(); // Create as draft
await tx.post(); // Post (affects balances)
// Transaction lifecycle
await tx.check(); // Mark as reconciled
await tx.uncheck(); // Unmark
await tx.trash(); // Move to trash
await tx.update(); // Update
// Query transactions
const txList = await book.listTransactions("account:'Bank' after:2024-01-01");
const transactions = txList.getItems();
Custom Properties
All resources support custom properties for bot integration:
// Set/get properties
tx.setProperty("exchange_rate", "1.25");
tx.getProperty("exchange_rate");
account.setProperty("bank_code", "001");
book.getProperty("base_currency");
Event Handling
Event Types
// Transaction events
TRANSACTION_CREATED; // Draft created
TRANSACTION_POSTED; // Posted to accounts
TRANSACTION_CHECKED; // Marked as reconciled
TRANSACTION_UNCHECKED; // Unmarked
TRANSACTION_UPDATED; // Modified
TRANSACTION_DELETED; // Trashed
TRANSACTION_RESTORED; // Restored from trash
// Account events
ACCOUNT_CREATED | ACCOUNT_UPDATED | ACCOUNT_DELETED;
// Other events
GROUP_CREATED | GROUP_UPDATED | GROUP_DELETED;
FILE_CREATED | FILE_UPDATED;
BOOK_UPDATED | BOOK_DELETED;
Event Handler Pattern
import { Hono } from "hono";
import { Bkper, Book } from "bkper-js";
const app = new Hono<{ Bindings: Env }>();
app.post("/events", async (c) => {
const event: bkper.Event = await c.req.json();
const bkper = new Bkper({
oauthTokenProvider: async () => c.req.header("bkper-oauth-token"),
agentIdProvider: async () => c.req.header("bkper-agent-id"),
});
const book = new Book(event.book, bkper.getConfig());
switch (event.type) {
case "TRANSACTION_CHECKED":
return c.json(await handleTransactionChecked(book, event));
default:
return c.json({ result: false });
}
});
TRANSACTION_CHECKED Handler Example
async function handleTransactionChecked(book: Book, event: bkper.Event): Promise<Result> {
const operation = event.data.object as bkper.TransactionOperation;
const transaction = operation.transaction;
if (!transaction?.posted) {
return { result: false };
}
// Prevent bot loops
if (transaction.agentId === "my-bot-id") {
return { result: false };
}
// Your logic here
console.log(`Transaction checked: ${transaction.id}`);
return {
result: `CHECKED: ${transaction.date} ${transaction.amount}`,
};
}
Response Format
type Result = {
result?: string | string[] | boolean; // Success message(s)
error?: string; // Error (red in UI)
warning?: string; // Warning (yellow in UI)
};
// Examples
{
result: false;
} // No action
{
result: "CHECKED: 2024-01-15 100.00";
} // Success
{
result: ["Book A: OK", "Book B: OK"];
} // Multiple
{
error: "Rate not found";
} // Error
App Configuration (bkperapp.yaml)
id: my-app
name: My App
description: Does something useful
logoUrl: https://example.com/logo.svg
website: https://example.com
ownerName: Your Name
ownerWebsite: https://yoursite.com
developers: your-username
# Menu integration (web handler)
menuUrl: https://${id}.bkper.app?bookId=${book.id}
menuUrlDev: http://localhost:8787?bookId=${book.id}
# Event handling (events handler)
webhookUrl: https://${id}.bkper.app/events
webhookUrlDev: https://${id}-dev.bkper.app/events
apiVersion: v5
events:
- TRANSACTION_CHECKED
Menu URL Variables
| Variable | Description |
|---|---|
${book.id} |
Current book ID |
${book.properties.xxx} |
Book property value |
${account.id} |
Current account ID |
${transactions.ids} |
Selected transaction IDs |
${transactions.query} |
Current query |
Common Patterns
Linking Transactions (remoteId)
// Create linked transaction
const mirrorTx = new Transaction(connectedBook)
.setDate(originalTx.date)
.setAmount(originalTx.amount)
.addRemoteId(originalTx.id) // Link to original
.post();
// Find linked transaction
const linked = await book
.listTransactions(`remoteId:${originalTx.id}`)
.then((list) => list.getFirst());
Bot Loop Prevention
// Always check agentId to avoid infinite loops
if (transaction.agentId === MY_AGENT_ID) {
return { result: false };
}
Connected Books
// Find related books in a collection
const collection = await book.getCollection();
const connectedBooks = collection?.getBooks() ?? [];
for (const connectedBook of connectedBooks) {
if (connectedBook.getId() !== book.getId()) {
// Process connected book
}
}
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.