A tenant can have many companies, and an extension installs into all of them at once. Code that assumes one company breaks on the second. Install and upgrade per company versus per database, OnCompanyInitialize, reading another company's data with ChangeCompany, tables that are DataPerCompany = false, and the job queue trap.
Everything in Business Central is per company by default: every table, every setup record, every number series. A tenant with five companies has five Item tables. An extension that was written and tested in Cronus and then installed into a group with twelve companies meets this on day one.
Install codeunits have [OnInstallAppPerCompany] (runs once in each company, now and for each company created later) and [OnInstallAppPerDatabase] (once per tenant). Upgrade likewise. Setup records, default data, retention policy registration: per company. Registering a feature key or anything in a DataPerCompany = false table: per database. Getting this wrong means a new company has no setup record (per-database install never ran for it) or a per-database table gets twelve identical rows.
When a company is created (admin centre or Companies page), the platform runs the per-company install for every installed app, and raises OnCompanyInitialize in Codeunit "Company-Initialize". Subscribe to it for anything that must exist in a fresh company (your setup record, your number series defaults, your report selections). Do not assume a company was created before your app was installed.
procedure GroupStockForItem(ItemNo: Code[20]) Total: Decimal
var
Company: Record Company;
ILE: Record "Item Ledger Entry";
begin
if Company.FindSet() then
repeat
ILE.ChangeCompany(Company.Name);
ILE.SetRange("Item No.", ItemNo);
ILE.CalcSums(Quantity);
Total += ILE.Quantity;
until Company.Next() = 0;
end;
ChangeCompany points a record variable at another company's table; reads and writes go there. Rules: the user needs permission in that company; triggers do not run on writes in the other company (so Insert(true) is Insert()); FlowFields work, CalcSums works; and it is not a transaction boundary, so a failure rolls back both. Use it for group reporting and intercompany, sparingly, and never from a posting routine.
DataPerCompany = false on a table makes one copy for the tenant: a shared log, a shared configuration, a cross-company queue. Use it when the data genuinely belongs to the tenant (an integration's token cache, a group-wide code list). Permissions still apply; retention still applies. The Company record itself, users and permission sets are per database.
A job queue entry belongs to the company it was created in and runs there. "The integration runs in the UK company but not the Irish one" is a job queue entry that exists in one company. Either create one per company (from the per-company install, if the job should run everywhere) or write one job that loops the companies with ChangeCompany and runs in a designated company. Document which.
Add a second company to the pipeline's container and run the install twice, and have at least one test that creates a company (insert into the Company table, then run Codeunit "Company-Initialize") and checks that your setup record and defaults exist in it. The second company is where multi-company bugs live, and the demo database only has one.