bcquality/microsoft/knowledge/events/changecompany-runs-triggers-in-the-calling-company.md
waldo b7617fb48a
knowledge(events): ChangeCompany leaves triggers and trigger-event subscribers running in the calling company (#152)
* knowledge(events): ChangeCompany leaves triggers and trigger-event subscribers running in the calling company

ChangeCompany redirects only the data access of a record variable; Learn states that triggers still run in the current company. The database trigger events are raised on every database operation and only pass RunTrigger to the subscriber, so Insert(false) after ChangeCompany still runs every subscriber in the calling company. Generated code either assumes the record 'becomes' a target-company record, or switches RunTrigger off and hand-copies the trigger logic, leaving the subscribers writing to the wrong company; none of the tested runs reached StartSession with the company parameter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* knowledge(events): qualify StartSession async semantics and fix concurrency-unsafe sample key

Addresses PR #152 review: StartSession is a fire-and-forget background
session (Ok reports only whether it started, not whether the codeunit
succeeded, and errors inside it do not propagate), so the Best Practice
now scopes the recommendation and calls out the durable status/error
channel a synchronous-success write needs. The good sample's
FindLast()+1 entry-number pattern raced under concurrent background
sessions; switched to AutoIncrement, which the platform guarantees is
unique across concurrent transactions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* knowledge(events): serialize the setup-counter increment; promote article and wire the review skill

PR #152 round 3 (JesperSchulz):
- The good sample's OnAfterInsertEvent subscriber still raced on the
  shared "Transfer Setup Good" singleton (Get/increment/Modify);
  AutoIncrement only protected the request key. Added
  TransferSetup.LockTable() before Get() to serialize concurrent
  background sessions.
- Promoted changecompany-runs-triggers-in-the-calling-company from
  community/knowledge/events/ to microsoft/knowledge/events/, and wired
  ChangeCompany/StartSession/RunTrigger tokens plus a targeted
  detection cue into microsoft/skills/review/al-events-review.md so a
  diff containing the anti-pattern reliably worklists this article,
  preserving the documented RunTrigger=false hand-off exception.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: waldo1001 <12088142+waldo1001@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 12:34:28 +02:00

6.3 KiB

bc-version domain keywords technologies countries application-area
all
events
changecompany
cross-company
runtrigger
trigger-event
subscriber
onafterinsertevent
insert
startsession
multi-company
al
w1
all

ChangeCompany leaves triggers and trigger-event subscribers running in the calling company

Contributions welcome — open a PR to refine or extend this article.

Description

ChangeCompany redirects the data access of one record variable to another company's table. Execution context does not move with it: Microsoft Learn states that triggers still run in the current company, not in the company passed to ChangeCompany. Code that knows this usually reaches for Insert(false) and copies the trigger's work by hand from the target company's setup. That closes only half of the gap. The runtime raises the database trigger events (OnBeforeInsertEvent, OnAfterInsertEvent, and their modify, delete, and rename counterparts) on every database operation and only passes the RunTrigger flag to the subscriber, so every subscriber that does not exit on RunTrigger = false still runs, in the calling company, against the calling company's setup, number series, and companion tables. The row lands in the target company, the side effects land in the caller, and nothing reports an error. The per-row cost of the call is a separate concern, see changecompany-in-loop-drops-caches.

Best Practice

Use ChangeCompany to read. Access rights in the target company are still enforced, so reads are safe. When the goal is business data in another company, run the code in that company: StartSession takes a company name and runs a codeunit there, so triggers, validation, and subscribers all execute with the target company as their context. StartSession is a background session, not a synchronous call: the Ok return value reports only whether the session started, not whether the codeunit's work inside it succeeded, the caller's transaction does not extend into it, and an error raised there does not come back to the caller — it has to be logged or telemetered from inside that session. Reach for StartSession only for work the caller does not need to confirm before it continues; a write whose success the caller must know synchronously needs a durable status or error channel (a field the caller polls, a job queue with retry) rather than a bare StartSession call. Learn notes that a background session costs as much as a user session to start, so batch the work rather than starting one session per row, or let the target company process a hand-off row on its own schedule. A direct cross-company write is acceptable only as such a hand-off into a table the writing extension owns, whose triggers do not read company data and whose trigger-event subscribers exit when RunTrigger is false, using Insert(false), Modify(false), or Delete(false), and never Validate.

See sample: changecompany-runs-triggers-in-the-calling-company.good.al.

Anti Pattern

An Insert, Modify, Delete, or Validate on a record variable after ChangeCompany(<name>), on a table whose triggers or trigger-event subscribers read setup, consume a number series, or write companion rows. With RunTrigger = true the trigger code fills the row from the caller's setup. With RunTrigger = false the trigger code is skipped, but the subscribers still fire in the caller, so a counter, log, or companion row maintained by a subscriber is written in the wrong company, and a caller that also updates the target by hand counts twice.

Detection signal: a record variable that has had ChangeCompany called on it with a company name and is later used with Insert, Modify, Delete, or Validate, where the table is not owned by the extension, or has triggers that read company data, or has trigger-event subscribers that do not exit on RunTrigger = false. Do not flag reads after ChangeCompany; writes with RunTrigger = false into an owned table whose triggers do not read company data and whose subscribers exit on RunTrigger = false; or ChangeCompany() without an argument, which points the variable back at the current company.

See sample: changecompany-runs-triggers-in-the-calling-company.bad.al.

See also