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

Reference / Workflow

fan-out (foreach subscribe)

subscribe Event(params) as Alias foreach (T x in [A, B, C]) { Candidates = u => …x…; }

One `subscribe` declaration that expands into MANY parallel wait slots — one per element of a collection. A fan-out over a literal enum list makes each instance addressable by the element's NAME (`Wf.For(entity).Architect.Event(…)`), and the element variable is in scope for the slot's `Candidates`.

stable1 example compiled by CIworkflowauthoring

Summary#

A foreach on a subscribe fans one declaration out into MANY parallel wait slots — one runtime slot per element of the collection. It is how a state waits on several symmetric approvers at once (three reviewers, N referees) without copy-pasting the slot three times. Over a literal enum list each instance is addressable by the element's member name, and the loop variable is in scope for the slot's Candidates.

Signature#

subscribe <Event>(<typed params>) as <Alias>
  foreach (<ElementType> <var> in [<A>, <B>, <C>]) {
    Candidates = <principal> => <predicate using var>;
  }

Description#

The foreach element type and variable are declared C#-style. At state entry the engine evaluates the collection and materializes one runtime slot per element, each tagged with that element as its key. The loop variable is a resolvable value in scope for the slot's settings — Candidates, Assignee, When — so a single predicate specializes per instance:

  • Addressing (literal enum list). The instance alias is the enum member name. Wf.For(entity).Architect targets the slot whose element is Role.Architect; its deposit verb is the subscribed event (Wf.For(entity).Architect.Vote(Decision.Approve, "lgtm")). The set of instance aliases is exactly the enum members in the list.
  • The element variable. Inside Candidates = u => u.Role == r, r is the element for THAT instance, so the Architect slot only accepts a principal whose Role == Role.Architect.

Each instance is an ordinary slot: it can be claimed, deposited into, and gated by Requires. The route that fires on a deposit is keyed on the event (on <Event> { … }) so all instances share one arm; a trailing Slot slot parameter on that route names WHICH instance answered (slot.Assignee is the depositor).

Casting into an instance does not, by itself, advance the state. Pair a fan-out with a state-level Requires — named preconditions, and the live checklist to express a quorum (“2 of 3 approvals”) — that requirement holding is the state's completion condition, so the remaining instances never have to answer.

A fan-out over a runtime collection (foreach (User u in this.Item.Topic.Referees)) is a separate, dynamic form: one slot per element resolved at state entry, each pre-assignable to its element, addressed by the acting principal rather than by a static name. See dynamic fan-out (foreach over a runtime collection).

Examples#

Three symmetric voter slots from one declaration, each gated to its own role, with a 2-of-3 quorum as the completion condition:

enum Decision { Approve, Reject }
enum Role     { Architect, Security, Product }
enum ReviewState { Gathering, Accepted, Rejected }

// The entity the workflow TRACKS, and the person it assigns slots to. The example referenced both
// and declared neither — which only went unnoticed while it was exempt from the gate.
[Principal]
entity Person {
  [Required, MaxLength(200)] string Email;
  Role Role = Role.Architect;
  security { allow read, create when IsAuthenticated; }
}

entity Design {
  [Required, MaxLength(160)] string Title;
  ReviewState Status;   // no default: the workflow owns this field (Tracks = Design.Status)
  [ForeignKey(Design)] Vote[] Votes;
  security { allow read, create, update when IsAuthenticated; }
}

entity Vote {
  [Required] Design Design;
  Person Voter;
  Decision Decision;
  [MaxLength(400)] string Comment;
  security { allow read, create when IsAuthenticated; }
}

workflow DesignReview {
  Tracks    = Design.Status;
  Autostart = true;
  Initial   = Gathering;

  event Vote(Decision decision, string comment);

  state Gathering {
    // one declaration → three parallel slots, addressable as .Architect / .Security / .Product
    subscribe Vote(Decision decision, string comment) as Voters
      foreach (Role r in [Role.Architect, Role.Security, Role.Product]) {
        Candidates = u => u.Role == r;
      }

    on Vote(Decision decision, string comment, Slot slot) {
      new Vote { Design = this.Item, Voter = slot.Assignee, Decision = decision, Comment = comment };
    }

    // the STATE completes when the quorum of approvals holds — the third voter never has to answer
    Requires {
      Quorum { Must = this.Item.Votes.Count(v => v.Decision == Decision.Approve) >= 2;
               Message = "Two of three approvals are required."; }
    }

    on Complete { goto Accepted; }
  }

  terminal success Accepted { }
  terminal error   Rejected { Message = "design rejected"; }
}

Driving it from a test (or a UI) — a voter casts on THEIR instance by its role name:

runas (Ada) { DesignReview.For(design).Architect.Vote(Decision.Approve, "lgtm"); }
runas (Sam) { DesignReview.For(design).Security.Vote(Decision.Approve, "ok"); }
// two of three approvals ⇒ Accepted; Product never voted

See also#

  • subscribe — the wait this fans out
  • Requires — named preconditions, and the live checklist — the state-level quorum that is the completion condition
  • <span class="planned" title="this page is planned and not written yet">workflow-route</span> — the event-keyed on <Event> { … } arm shared by every instance (and its Slot slot param)
  • <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state

Related

dynamic fan-out (foreach over a runtime collection)

A `subscribe … foreach` over a RUNTIME entity collection expands into one wait slot per element of the collection…

subscribe

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

Requires — named preconditions, and the live checklist

Named conditions that must hold before something may happen, declared on a state or on a slot — and readable as a live…