Entities
CRUD against the platform's entities — Invoice, Customer, Payment, and the rest.
The generic Entity class plus per-entity classes (Invoice, Customer, Payment, …) are how scripts read and write data. All are exported from @backfill-io/sdk. Both shapes hit the same data — the per-entity version is shorter and type-aware.
import { Entity, Invoice, Customer } from "@backfill-io/sdk";
The examples below assume these imports.
The interface
interface EntityClass<TRecord, TCreateInput, TUpdateInput> {
get(id: string, opts?: { include?: string[] }): TRecord | null;
query(opts?: EntityQueryOptions<TRecord>): TRecord[];
create(attrs: TCreateInput): TRecord;
update(id: string, attrs: TUpdateInput): TRecord;
delete(id: string): boolean;
exists(where?: Record<string, any>): boolean;
count(where?: Record<string, any>): number;
action(id: string, actionName: string, params?: Record<string, any>): any;
}
interface EntityQueryOptions<TRecord> {
where?: Partial<TRecord> | Record<string, any>;
orderBy?: Array<{ field: string; direction: "asc" | "desc" }>;
limit?: number;
offset?: number;
include?: string[];
}
Generic vs. shortcut
// generic
const inv = Entity.get("Invoice", "inv_123");
const recents = Entity.query("Invoice", { where: { status: "open" }, limit: 50 });
// per-entity — same data, less typing
const inv2 = Invoice.get("inv_123");
const recents2 = Invoice.query({ where: { status: "open" }, limit: 50 });
Custom entities you declare via defineEntity get the same treatment — once the CLI regenerates types, they’re importable by their PascalCase name.
Reads
get
const inv = Invoice.get("inv_123");
// → InvoiceRecord | null
const withCustomer = Invoice.get("inv_123", { include: ["customer"] });
query
const open = Invoice.query({
where: { status: "open" },
orderBy: [{ field: "issued_at", direction: "desc" }],
limit: 50,
});
where accepts simple equality. Range filters use system fields with operators:
const recent = TaxRecord.query({
where: { _created_at: { gte: "2026-04-01" } },
});
exists, count
if (!Customer.exists({ email: input.email })) {
Customer.create({ email: input.email, name: input.name });
}
const total = Invoice.count({ status: "open" });
Writes
create
const inv = Invoice.create({
customer_id: "cus_123",
currency: "USD",
lines: [
{ description: "Consulting", amount: "1000.00", line_type: "service" },
],
});
Required fields differ per entity — your editor’s TypeScript inference will tell you what create accepts. For custom entities, the required: true flags in defineEntity apply.
update
Invoice.update("inv_123", { status: "paid" });
delete
Invoice.delete("inv_123");
// → true if it existed
Actions
action invokes an entity-specific operation that’s not a plain CRUD verb — for example, posting an invoice to the ledger.
Invoice.action("inv_123", "post", { effective_date: "2026-05-01" });
The set of actions varies by entity.
Permissions
Every Entity.* call checks the manifest’s permissions.entities. With:
permissions: { entities: { Invoice: ["read"] } }
Invoice.query(...) works. Invoice.update(...) throws. Add "write" to permit writes.