# Child fan-out

`schedule_childs` spawns a batch of child instances and parks the parent on a **join barrier**
that releases only when every child reaches a terminal status. It is "fan work out, then wait
for all of it."

```elixir
def step("fan", %{state: s}) do
  children = for kid <- s["kids"], do: {LineItem, state: kid}
  {:schedule_childs, "join", children, s}
end

def step("join", %{childs: childs}) do
  {:done, %{"shipped" => Enum.count(childs, &(&1.status == "done"))}}
end
```

`{:schedule_childs, next_step, children, state}`:

- **`children`** is a list of child specs, each `{FsmModule, insert_opts}` (or a bare
  `FsmModule`). They are ordinary instances, stamped with a `parent_id`.
- The spawn and the park happen **atomically** — children become runnable only once the parent
  is parked, so a child can never finish before the barrier exists.
- The parent re-runs **`next_step`** (never the spawning step) once all children are terminal.

## The join

When the parent wakes, its children are in `ctx.childs`:

```elixir
[%{id: _, fsm: _, status: "done" | "failed", state: _, result: _, last_error: _}, ...]
```

The barrier waits for **termination, not success** — a child that ends `failed` still releases
its slot. The parent sees the failure in `ctx.childs` and decides what to do (proceed,
`{:stop, _}`, compensate). The engine does not auto-propagate child failure.

`ctx.childs` holds **all** of the instance's children still in the database — including
those from earlier `schedule_childs` rounds (until [GC](operations.md) prunes them). A
parent that fans out repeatedly should filter by its own bookkeeping (e.g. ids or a marker
in the child state) rather than assume the list is the latest batch.

## Not covered

- **No cancel/cascade** — terminating or failing a parent does not touch its children, and
  vice-versa.
- **No quorum** — the join is all-or-nothing; build `k`-of-`n` yourself with
  [`await`](signals.md) + per-child signals.
- **No barrier deadline** — a child that hangs forever hangs the parent; bound it inside the
  child (e.g. `{:retry, ...}` with a timeout).
- **Nesting is free** — a child may itself `schedule_childs`, each level an independent barrier.

Children are ordinary terminal rows after they finish; they are reaped by
[GC](operations.md#garbage-collection) like any other terminal instance, not deleted on read.
