Support came to us asking who cancelled an order. We had the event. It said CANCEL, and that was the whole of it.
The actor was in there somewhere, on a field of the payload, written by whichever code path got there first: sometimes a role, sometimes a service name, sometimes empty. Four different things could cancel an order in that system. The customer changed their mind, the merchant rejected it, the payment path failed, finance wrote it off. All four ended up in the same place, so all four used the same trigger. The state was right. The story was gone.
We rebuilt it the other way round in our open-source project order-lifecycle. The transition table is keyed by the pair (source state, trigger), and the constructor indexes every row by that pair and raises on a duplicate, so two rows can never claim the same key. The cancellation policy applies the same rule to its own paths, and the error message says the reason out loud: give each actor its own trigger. Customer gets CANCEL. Merchant gets REJECT. The payment path gets FAIL. Finance gets WRITE_OFF. Each path becomes a row in the table, its follow-up work becomes after hooks, and resolution walks the paths in declaration order and returns the first one that permits the calling role.
The actor is legible in the history now, as the name of the trigger. Nothing to join, nothing to trust.
Two things that cost us.
The history lookup compares trigger equality. Ask it for CANCEL and you get nothing that was recorded under REJECT, FAIL or WRITE_OFF. Every "cancellation rate" query has to name the whole set now. We stopped filing that as a bug, because the set is the honest answer, and the old single number was four different business events wearing one label.
The second one we shipped, and we still live with it. The cancel call checks the policy, not the machine. It resolves the path, accepts the role, accepts the reason, then applies the trigger, and if the machine was built on a table that was never extended with the policy's rows, that apply raises an illegal-transition error. Permission granted, transition refused, one layer too late. Build the machine on the extended table and it does not happen. But the failure surfaces at apply time rather than at construction, which is probably the wrong place for a wiring mistake, and we know it.
If you keep an audit trail of state changes, you encode the actor either in the event name or as a field on the event. The field is easier to aggregate. The name is the only version that cannot drift.