# fan-out (foreach subscribe)

> 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`.

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

## Summary        {#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      {#signature}
```osy syntax
subscribe <Event>(<typed params>) as <Alias>
  foreach (<ElementType> <var> in [<A>, <B>, <C>]) {
    Candidates = <principal> => <predicate using var>;
  }
```

## Description    {#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](https://osysharp.com/reference/workflow/requires/) 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)](https://osysharp.com/reference/workflow/fan-out-dynamic/).

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

```osy title="three role-gated slots from one declaration" test app=workflow-fan-out
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:

```osy title="a voter casts on their own instance by role name" syntax
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       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the wait this fans out
- [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) — 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
