# actor — who just did this

> Inside any workflow body, `actor` is the principal whose action drove this dispatch — who deposited the event, who raised it, whose write started the run. It is nothing when the engine drove the body itself: a reminder, an SLA breach, a deadline, a callback from someone with no account. It is deliberately NOT the slot's `Assignee`, which answers a different question — whose queue the work sat in — and gives a different answer whenever the two diverge.

<!-- id: workflow-actor · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/actor/ -->

## Summary        {#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`](https://osysharp.com/reference/workflow/assign/), 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      {#signature}
```osy syntax
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    {#description}

### Two questions, two answers   {#two-questions}
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   {#pool-slot}
A pool slot — one with [`Candidates`](https://osysharp.com/reference/workflow/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:

```osy title="crediting the decision to who made it" test app=workflow-actor-pool
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   {#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:

```osy title="a cancel names who cancelled it" syntax
on Cancel { this.Item.CancelledBy = actor; goto Cancelled; }
```

### When the engine acts, `actor` is nothing   {#engine}
A body the **engine** drives has no acting principal, and `actor` is nothing there. That covers every clock-driven and
unattended path:

- a [reminder](https://osysharp.com/reference/workflow/remind/) firing;
- a [milestone breach](https://osysharp.com/reference/workflow/milestone/) — `Unassigned`, `Unfinished`, `Exhausted`;
- an `Expire` or `Deadline` elapsing;
- a [callback URL](https://osysharp.com/reference/workflow/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](https://osysharp.com/reference/workflow/audit/) records the event kind, and a slot
records how it was satisfied.

A body that must behave differently when nobody acted asks directly:

```osy title="a body that runs both ways" syntax
Unassigned {
  if (actor == null) { Escalate(); return; }   // the clock got here first
  Notify(actor);
}
```

### It survives a park   {#replay}
`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](https://osysharp.com/reference/workflow/body-security/)): 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   {#trustworthy}
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](https://osysharp.com/reference/workflow/audit/) records for the same event, so a hand-written record and the
platform's own timeline cannot disagree.

### Where it is not in scope   {#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 `Candidates` predicate.** 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       {#examples}

### Naming the actor on a slot somebody else holds   {#outsider}
The discriminating case. The slot is assigned to one person; somebody else acts on it. Both facts are recorded, and
they are different:

```osy title="who it was assigned to, and who actually did it" test app=workflow-actor-outsider
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       {#see-also}
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — who may hold or satisfy a slot
- [Assign — handing a slot to a named colleague](https://osysharp.com/reference/workflow/assign/) — moving a slot to somebody, and what `Assignee` means
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — the run's timeline, which records the same actor
- [What a workflow body may write](https://osysharp.com/reference/workflow/body-security/) — why a body runs on the engine's authority, and what that means for reads
- [Acting on an inbox row (deposit, claim, release)](https://osysharp.com/reference/workflow/inbox-act/) — claiming, releasing and acting from a queue
