Osy#betaRun it on Osyrin: osyrin.com

Reference / Workflow

Raising a workflow event

<Workflow>.Raise<Event>(entity, args…) · Workflow.Raise(entity, Event(args…))

Raise a typed event on the run bound to an entity, from anywhere — an ordinary server function, a webhook handler, a signup step. Two spellings do it: the NAMED form states which workflow, and the INFERRED form works it out from the entity's type, which is what generic code wants when it should not have to know.

stable2 examples compiled by CIworkflowauthoring

Summary#

An event is how the outside world moves a workflow along. Raising one advances the run bound to an entity — routing to the slot in the current state that subscribes it, or to a workflow-level arm that handles it wherever the run has got to.

There are two spellings, and the only difference is whether the call names the workflow:

OrderFlow.RaisePayment(order, 100);       // NAMED    — you say which workflow
Workflow.Raise(order, Payment(100));      // INFERRED — worked out from the entity's type

Both drive the same engine, enforce the same [[workflow-authorize|[Authorize]]] gate, and route the same way. Neither is a test-only surface: an ordinary server function may raise an event, and that is the normal way an app advances a run from outside the workflow.

Signature#

<Workflow>.Raise<Event>(entity, args…)    // named: the method is the workflow's, one per declared event
Workflow.Raise(entity, <Event>(args…))    // inferred: the event is written as a constructor call

entity is the row the run is bound to — whatever the workflow Tracks. The arguments are the event's declared parameters, in order, and they arrive in the arm by name.

Description#

Which spelling to use#

Reach for the named form by default. It is the one that reads back as what it does — the workflow is on the page, so anyone changing the model can find every producer of an event by searching for it, and nothing about the call depends on facts elsewhere in the model.

Reach for the inferred form when the caller genuinely should not know the workflow: shared helpers, generic plumbing, anything written against "an entity that has a workflow" rather than against one particular flow. It is narrower than it looks — most application code knows perfectly well which workflow it is advancing, and writing that down costs one identifier.

⚠ The inference needs the entity's type to have exactly ONE workflow#

The inferred form resolves the workflow from the entity's type. If two workflows bind that type, there is nothing to infer, and the compiler refuses the call rather than picking one:

Workflow.Raise: more than one workflow in this unit binds 'Ticket', so the workflow cannot be inferred from the
entity — name it instead: `<Workflow>.RaiseResolve(t)`

This is a compile error on purpose, and the reason is the same as the reason the form exists. Generic code is precisely the caller that cannot check: a helper holding somebody else's entity has no way to notice that the type has since acquired a second workflow. So the check belongs where the whole model is in view. Adding a second workflow to a type is a change that will name every inferred call that has just become ambiguous, and each is a one-word fix.

Where it routes#

Raising does not name a slot. The engine takes the event to:

  • the slot in the run's current state that subscribes it — the ordinary case; or
  • a workflow-level arm for it, wherever the run has got to — how a Cancel reaches a run in any state.

An event no slot is waiting for and no workflow-level arm handles is refused, not queued. To answer a specific queued slot from a person's inbox — where the row, not the code, decides which slot is being answered — use Workflow.Deposit(row, …) instead.

The run has to exist#

Both forms raise on the run already bound to the entity: they do not start one. If nothing has started a run for that row, the call fails saying so — Workflow.Run(entity) (or Autostart) is what creates it. When a row has more than one run over its lifetime, a run still waiting is preferred over a finished one.

From the run's own body — delivered when the step ends#

A body of the workflow — an on arm, a Remind, a breach handler — may raise an event on the run it belongs to: a reminder that asks a carrier where a parcel is and raises Arrive once it hears "delivered", or an arm that forwards one event as another. The raise is queued, and delivered as soon as the body's step ends — in the same call that ran the body (the same Workflow.Settle, the same deposit) — and it then behaves exactly like a raise from outside: the same authorization, the same routing.

It is one step later rather than in the middle of the body because the body is itself part of a step of that run: the run moves after the step it is in, never inside it. If the event is not one the run's state takes by then, the raise is refused, and a Refused entry naming the event lands on the run's timeline.

A body raising on a different run is unaffected: that raise is delivered where it is written. A body cannot deposit into, claim, assign or release a slot of its own run — each is refused by name; raise an event instead.

Authorization is the workflow's, not the caller's#

An event's [Authorize] predicate and a slot's Candidates are enforced by the engine, so they hold identically for both spellings and for a raise from ordinary application code. Being able to call the function is not permission to advance the run.

Who a raise from a workflow body is from#

A raise written in a workflow BODY — an app's lifecycle telling a kit's run "we answered" from its on Reply { … } — is from the person whose action drove that body: the same actor the body reads, and the same answer Workflow.Run gives a child. The target's [Authorize] and Candidates are asked about THAT person, exactly as if they had raised it themselves, so nothing is widened. A body driven by a timer, a reminder or a breach has no actor: its raise into a gated event or a Candidates slot is refused, and an ungated event takes it — so a run that must be moved on a timer is moved by an ungated event of its own.

On an item with runs of two workflows, <Wf>.RaiseX(item) reaches that workflow's run.

Examples#

An ordinary server function advancing a run — the signup / webhook shape, with the workflow named:

using Osysharp.Workflow;

enum TicketStatus { Working, Closed }

entity Ticket {
  [Required, MaxLength(200)] string Title;
  TicketStatus Status = TicketStatus.Working;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow TicketFlow {
  Tracks  = Ticket.Status;
  Initial = Working;
  event Resolve();
  state Working {
    subscribe Resolve();
    on Resolve { goto Closed; }
  }
  terminal success Closed { }
}

void ResolveTicket(Ticket t) {
  TicketFlow.RaiseResolve(t);
}

The same call without naming the workflow, and with an argument the arm routes on:

using Osysharp.Workflow;

enum ExpenseStatus { Filed, Approved, Rejected }

entity Expense {
  [Required, MaxLength(200)] string Memo;
  ExpenseStatus Status = ExpenseStatus.Filed;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow ExpenseApproval {
  Tracks  = Expense.Status;
  Initial = Filed;
  event Decide(bool approved);
  state Filed {
    subscribe Decide(bool approved);
    on Decide(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }
  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}

void DecideExpense(Expense e) {
  Workflow.Raise(e, Decide(true));
}

See also#

Related

Workflow.Run (start a workflow)

Start the workflow bound to an entity's type, on that entity. Bare — `Workflow.Run(order)` — is fire-and-forget: start…

subscribe

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

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…

[Authorize] (event)

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