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.