Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Workflow

What a workflow body may write

state X { on Event { this.Item.Anything = …; } } — a body writes as the SYSTEM, not as the caller

A workflow body runs on a system data context, so its writes are NOT gated by the entity's `security {}` block. The person who raised the event is authorized to RAISE it — `[Authorize]` and a slot's `Candidates` decide that — and everything the transition then writes lands regardless of what that person could have written themselves.

stable1 example compiled by CIworkflowsecurity

Summary#

A workflow body writes as the system, not as the caller. The entity's security {} block gates what a PERSON may do directly; it does not gate what a transition does once an event has been legitimately raised.

Signature#

state Open {
  on Close {
    this.Item.Note = "closed by the workflow";   // lands even if NO rule grants `update` to anyone
    goto Closed;
  }
}

Description#

Where the authorization actually happens#

Two different questions, and only the first one is about the person:

  • May they raise this event? [Authorize(principal => …)] on the event, and a slot's Candidates for who may hold the work. See [Authorize] (event).
  • May the body write this row? Not asked. The engine opens a system data context for the run, and a system context is defined as one that opts out of data security.

So a transition is trusted code, in the same sense a server function's own bookkeeping is. Put the gate on the EVENT, where the person is.

Why it is built that way#

A run outlives the request that started it. A milestone that expires at 2am, a deadline, a retry after a service came back — these resume with no acting principal at all, and a rule written allow update where Owner == user has no user to compare against. If a body were gated by the caller's rules, the same transition would succeed or fail depending on who happened to trigger it, and a timer-driven one could never succeed.

What this means for your model#

  • Do not rely on security {} to stop a workflow. If a field must never change once a run owns it, that is a guard in the body or an [Immutable] on the member — not a missing allow update.
  • A read from a page is still gated. This is about the BODY. What the person then sees on screen goes through the ordinary read rules, unchanged.
  • [Authorize] is the real perimeter. An event anybody may raise is an event anybody may cause every write in its transition to happen.

Examples#

A whole app, compiled: Ticket grants read and create and no update to anyone, and the transition writes Note anyway.

enum TicketState { Open, Closed }

entity Ticket {
  [MaxLength(80)] string Title;
  TicketState Status = TicketState.Open;
  [MaxLength(200)] string Note = "";
  security { allow read, create when IsAnonymous || IsAuthenticated; }   // no `update`, to anyone
}

workflow TicketFlow {
  Tracks  = Ticket.Status;
  Initial = Open;

  event Close();

  state Open {
    on Close {
      this.Item.Note = "closed by the workflow";   // lands: a body is not gated by the block above
      goto Closed;
    }
  }
  state Closed { }
}

void CloseIt(Ticket t) { TicketFlow.RaiseClose(t); }

Gate the EVENT, which is where the person is:

[Authorize(principal => principal == this.Item.Owner)]
event Close();

See also#

Related

[Authorize] (event)

Gates WHO may raise a workflow event. `[Authorize]` on an event is a `principal => bool` predicate over the acting…

subscribe

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

The security model

How authorization works in Osy#, end to end. Everything is denied until you grant it; a grant is compiled into every…

security { }

The rules that decide who may read and write an entity's rows. A where clause filters by the row (the owner sees their…