Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / Workflow

actor — who just did this

actor // the principal whose action drove this workflow body, or nothing

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.

stable2 examples compiled by CIworkflowauthoringsecurity

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:

situationactor<Slot>.Assignee
a claimed slot, decided by its holderthe holderthe holder
a pool slot decided with no claimthe depositornothing — it was never anybody's
a slot assigned to one person, acted on by anotherthe person who actedthe person it was assigned to
a reminder, breach, deadline or expirynothingwhoever holds it, if anyone
a route with no slot at all (on Cancel)whoever raised itthere 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:

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 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#

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#

Related

Candidates (slot)

Declares WHO may hold or satisfy a `subscribe` slot. `Candidates` is one expression surface that dispatches on its…

Assign — handing a slot to a named colleague

Give a slot to somebody else. Claiming takes work for yourself and releasing puts it back in the pool; assigning is the…

For(entity).Audit

Reads a running instance's lifecycle timeline — every transition, claim, deposit, reminder and refusal as an…

What a workflow body may write

A workflow body runs on a system data context, so its writes are NOT gated by the entity's `security {}` block. The…

Acting on an inbox row (deposit, claim, release)

Answer a queued slot from the row itself. The event is named at the call site because a queue's rows are heterogeneous…

subscribe

Declares that a workflow state waits on an event, and configures the wait — who may hold it, who may hand it on…