Summary#
actor is the ambient principal of a workflow body: the person whose action caused this body to run. It is in
scope in a Start, a state enter/exit, a route arm and a milestone body, and it is typed as the app's
[Principal] entity.
It answers who just did this. That is a different question from Assignee, which answers
whose queue was this in — and the two give different answers exactly where it matters.
actor is nothing when the engine drove the body rather than a person. That is a real answer, not a gap.
Signature#
on <Slot>(<payload>) {
this.Item.DecidedBy = actor; // WHO DID IT — the principal who deposited
this.Item.Queue = <Slot>.Assignee; // WHOSE QUEUE — nothing on a pool slot nobody claimed
}Description#
Two questions, two answers#
A slot is a place work waits. Assignee names who is holding that place. actor names who performed the act the
body is running for. Most of the time they are the same person and the distinction is invisible — which is exactly why
it is worth stating, because the cases where they differ are the ones anybody ever asks about later:
| situation | actor | <Slot>.Assignee |
|---|---|---|
| a claimed slot, decided by its holder | the holder | the holder |
| a pool slot decided with no claim | the depositor | nothing — it was never anybody's |
| a slot assigned to one person, acted on by another | the person who acted | the person it was assigned to |
| a reminder, breach, deadline or expiry | nothing | whoever holds it, if anyone |
a route with no slot at all (on Cancel) | whoever raised it | there is no slot to ask |
Reading Assignee when you meant actor is the single commonest way a workflow records the wrong person, and it
fails silently: on a pool slot it writes nothing at all, and on an assigned slot it writes a plausible name that is
not the one who acted.
A pool slot decided without a claim#
A pool slot — one with Candidates and no Assignee — belongs to nobody until somebody
calls Claim(). Depositing its event satisfies the slot without ever assigning it, so Assignee is still nothing
inside the arm. actor is who deposited it:
enum PoStage { Review, Done }
enum Dept { Legal, Finance }
[Principal] entity Person {
[Required, MaxLength(80)] string Name;
Dept Department = Dept.Legal;
security { allow read, create when IsAuthenticated; }
}
entity Po {
[Required, MaxLength(120)] string Title;
PoStage Stage;
Person DecidedBy;
security { allow read, create, update when IsAuthenticated; }
}
workflow PoApproval {
Tracks = Po.Stage;
Autostart = true;
Initial = Review;
event Approve();
state Review {
subscribe Approve() as Legal {
Candidates = u => u.Department == Dept.Legal;
}
// `Legal.Assignee` is nothing here — nobody claimed it. `actor` is who approved.
on Legal { this.Item.DecidedBy = actor; goto Done; }
}
terminal success Done { }
}There is no ceremony to write. You do not need to claim the slot on the way in to have somebody to credit, and claiming in order to record an actor would write down a falsehood: it would record whoever acted as the assigned approver, which on a pool slot they never were.
A route with no slot#
An event route that is not keyed to a slot — a cancel, an escalation, anything raised on the run as a whole — has no
slot to ask at all. actor is the only thing that can answer, and it does:
on Cancel { this.Item.CancelledBy = actor; goto Cancelled; }When the engine acts, actor is nothing#
A body the engine drives has no acting principal, and actor is nothing there. That covers every clock-driven and
unattended path:
- a reminder firing;
- a milestone breach —
Unassigned,Unfinished,Exhausted; - an
ExpireorDeadlineelapsing; - a callback URL redeemed by a third party who has no account.
⚠ It is nothing, not "the last person who touched this run." A ticket that Lena submitted and that then breached its SLA overnight was not breached by Lena, and a timeline saying so is worse than one saying nothing. If you need to distinguish which unattended path it was, the audit trail records the event kind, and a slot records how it was satisfied.
A body that must behave differently when nobody acted asks directly:
Unassigned {
if (actor == null) { Escalate(); return; } // the clock got here first
Notify(actor);
}It survives a park#
actor is a fact of the dispatch, recorded when the action happened — not a question about who is signed in now.
A body that parks on an await and resumes days later still names the same person. That is why Session.CurrentUser
is the wrong question inside a workflow body and is correctly nothing there (see What a workflow body may write): a
resuming body runs on the engine's own authority, with nobody signed in to ask about.
A body started by another body — a saga child, a Workflow.Run — inherits the actor of the body that started it,
because the person's action is what caused it to exist.
It is the caller's, and app code cannot set it#
The value comes from the authenticated principal at the raising call and travels engine-side only. There is no assignment form, no argument that carries one, and nothing in the language that can name a different person. So it is the value to record when the question is "who approved this" and somebody will one day need the answer to be true.
actor is also what the audit trail records for the same event, so a hand-written record and the
platform's own timeline cannot disagree.
Where it is not in scope#
- Outside a workflow body — an ordinary function, an action, a page. There the caller is the current principal:
write
Session.CurrentUser. - In a
Candidatespredicate. That predicate decides about a principal rather than running because of one: it is evaluated for every candidate when an inbox is listed, and once per principal at the deposit gate. The principal under test is the predicate's own parameter, so compare against that.
A local or parameter named actor shadows the ambient one, exactly as it would shadow any other name.
Examples#
Naming the actor on a slot somebody else holds#
The discriminating case. The slot is assigned to one person; somebody else acts on it. Both facts are recorded, and they are different:
enum Stage { Held, Done }
[Principal] entity Person {
[Required, MaxLength(80)] string Name;
security { allow read, create when IsAuthenticated; }
}
entity Claim {
[Required, MaxLength(120)] string Title;
Stage State;
[Required] Person Owner;
Person DecidedBy; // who acted
Person Queue; // whose queue it sat in
security { allow read, create, update when IsAuthenticated; }
}
workflow ClaimFlow {
Tracks = Claim.State;
Autostart = true;
Initial = Held;
event Decide();
state Held {
subscribe Decide() as Reviewer {
Assignee = this.Item.Owner;
}
on Reviewer {
this.Item.DecidedBy = actor; // the person who stepped in
this.Item.Queue = Reviewer.Assignee; // the person it was waiting on
goto Done;
}
}
terminal success Done { }
}See also#
- Candidates (slot) — who may hold or satisfy a slot
- Assign — handing a slot to a named colleague — moving a slot to somebody, and what
Assigneemeans - For(entity).Audit — the run's timeline, which records the same actor
- What a workflow body may write — why a body runs on the engine's authority, and what that means for reads
- Acting on an inbox row (deposit, claim, release) — claiming, releasing and acting from a queue