How to generate N items per parent
You want each generated record to carry a variable-length array of child values: a handful of tags on a user, several line-items on an order, a few phone numbers on a contact. The repeat step inside a composition produces exactly that — an array, drawn deterministically from a seed.
Steps
- Add a
repeatstep to a composition'sletblock. Its shape is{ "repeat": { "count": …, "of": … } }:count— how many items to produce (see the variations below).of— the generator to invoke for each item (any PGDL generator).
- Reference the step's name from the composition's
output(orbody). The step is bound to the resulting array. - Load the package with
--packageand run it with--use.
Complete example
A user with three tags. Save this as repeat-pkg/phony.json:
{
"name": "@demo/repeat",
"version": "0.1.0",
"generators": [
{ "name": "@demo/repeat:user_with_tags",
"let": {
"tags": { "repeat": { "count": 3, "of": {
"type": "list", "source": "inline",
"values": ["urgent","new","sale","vip","beta"] } } }
},
"output": { "id": "{{ pattern:U#### }}", "tags": "{{ tags }}" } }
]
}Run it:
phony generate --use @demo/repeat:user_with_tags --package ./repeat-pkg --seed 42 -n 2Actual output:
[
{
"id": "U6377",
"tags": [
"sale",
"beta",
"beta"
]
},
{
"id": "U0248",
"tags": [
"new",
"sale",
"sale"
]
}
]Each item is drawn independently, so repeats within one array are expected (see unique to forbid them).
Variations: the four ways to set count
count accepts a literal, a range string, or a PEL expression.
A fixed number
{ "repeat": { "count": 3, "of": { "type": "logic", "algorithm": "int_between", "params": { "min": 1, "max": 99 } } } }Always three items.
A range "a-b" (drawn per record)
A string "a-b" draws an inclusive count per invocation, so different rows get different lengths:
{ "name": "@demo/repeat2:cart_range",
"let": {
"items": { "repeat": { "count": "1-4", "of": {
"type": "logic", "algorithm": "int_between", "params": { "min": 1, "max": 99 } } } }
},
"output": { "items": "{{ items }}" } }phony generate --use @demo/repeat2:cart_range --package ./repeat2 --seed 1 -n 3[
{ "items": [ 63 ] },
{ "items": [ 60, 1, 61 ] },
{ "items": [ 90, 11, 30 ] }
]One, three, three — each row's length is its own draw, always within 1..=4.
A PEL expression (count from a param or let)
Any other string is a PEL expression evaluated against the composition's locals, so the count can come from a param:
{ "name": "@demo/repeat2:order",
"params": { "n_items": { "type": "integer", "default": 3 } },
"let": {
"lines": { "repeat": { "count": "n_items", "of": {
"type": "list", "source": "inline",
"values": ["Widget","Gadget","Sprocket","Cog","Bolt"] } } }
},
"output": { "order_id": "{{ pattern:ORD-#### }}", "lines": "{{ lines }}" } }Pass the param through an input file (--use can't carry params) — save as order.json:
{ "use": "@demo/repeat2:order", "params": { "n_items": 5 } }phony generate ./order.json --package ./repeat2 --seed 7 -n 1[
{
"lines": [ "Sprocket", "Cog", "Cog", "Sprocket", "Widget" ],
"order_id": "ORD-6857"
}
]With n_items omitted the default (3) applies.
Distinct items (unique: true)
Add "unique": true to de-duplicate an array. Items are re-rolled until each is distinct:
{ "name": "@demo/repeat2:distinct_tags",
"let": {
"tags": { "repeat": { "count": 3, "unique": true, "of": {
"type": "list", "source": "inline",
"values": ["urgent","new","sale","vip","beta"] } } }
},
"output": { "tags": "{{ tags }}" } }phony generate --use @demo/repeat2:distinct_tags --package ./repeat2 --seed 42 -n 2[
{ "tags": [ "sale", "beta", "new" ] },
{ "tags": [ "new", "sale", "beta" ] }
]Items that reference the parent
The of generator's params are PEL templates evaluated against the composition's locals, so an item can read a parent value. Here every line's price is drawn from a per-record base:
{ "name": "@demo:order_lines",
"let": {
"base": { "computed": "100" },
"lines": { "repeat": { "count": 2, "of": {
"type": "logic", "algorithm": "int_between",
"params": { "min": "{{ base }}", "max": "{{ base }}" } } } }
},
"output": { "lines": "{{ lines }}" } }Both items read base, so both are 100.
Nesting
of can be { "use": "@your/other:composition" }, so a repeat of a composition nests arrays of structured records without limit — e.g. an order that repeats line-items, each line-item itself an output object.
Pitfalls
countmust resolve to a number. A literal integer, an"a-b"range string, or an expression that evaluates to a number. A range whose bounds aren't integers is treated as a plain expression, not a range.unique: truecan run out of room. If the item generator's domain is smaller thancount, Phony gives up after a bounded number of retries and fails with a clear error — widen the item generator's range.- A negative or zero count yields an empty array, never an error.
- References are checked at load time. A typo in the
ofgenerator's params (a name that isn't a param orlet) fails when the package loads, not mid-generation.