ApexBlueprint: Relations, bulk generation, and reference patterns
In Declaring a Single Record with SBlueprint, we covered the five core methods for declaring a single record on its own. This page goes one step further and covers patterns for structurally assembling multiple records.
Specifically, the following topics:
withChildren: nesting children inside a parent blueprinttimes(n)+{#}placeholder: bulk generation with sequence numbersusewith offsets: linking only some bulked parents to children{P0}/{P1}: referencing ancestors (immediate parent / grandparent) inside nestingparentIdField: disambiguating which lookup to fill when the child has multiple parent lookupssharedWith: declaring manual shares, and how they follow bulk generation
withChildren: Nesting Children Under a Parent
withChildren(child) lets you write "the child records of this blueprint" inline inside a single blueprint. The indentation hierarchy of your code becomes the data hierarchy directly, making parent-child relationships readable at a glance.
Simple 1:1 Parent-Child
SBlueprint.of(Account.class)
.template(Blueprints.accBasic())
.alias('parentAccount')
.withChildren(
SBlueprint.of(Contact.class)
.set('LastName', 'TestContact')
.alias('childContact')
);
Points:
- Copying the parent's Id into the child's
AccountIdis automatic. There's no need to write.use(...) - Defining the child blueprint inline inside
withChildrenvisualizes the structure of "this parent has this child hanging under it" - If you attach
.alias(...)on the child side, you can retrieve it viagetByAlias('childContact')
1:N Parent-Child (Combined with times)
Using .times(n) inside withChildren lets you hang multiple children under the same parent.
SBlueprint.of(Account.class)
.alias('parentAccount')
.template(Blueprints.accBasic())
.withChildren(
SBlueprint.of(Contact.class)
.set('LastName', 'Contact-{#}')
.alias('con_{#}')
.times(3)
);
Three Contacts — con_1 / con_2 / con_3 — are generated, all linked to the same Account. Because {#} is used both in the alias and the LastName, you can later retrieve them individually via getByAlias('con_2').
Nesting Further to Grandchildren
withChildren is nestable. Three-tier structures like "Contact under Account, Case under Contact" feel natural to write.
SBlueprint.of(Account.class)
.alias('acc')
.template(Blueprints.accBasic())
.withChildren(
SBlueprint.of(Contact.class)
.set('LastName', 'TestContact')
.alias('con')
.withChildren(
SBlueprint.of(Case.class)
.set('Subject', 'TestCase')
.alias('case')
)
);
Multiple Child Types Under the Same Parent
To hang children of different SObject types under the same parent, call .withChildren(...) repeatedly.
SBlueprint.of(Account.class)
.template(Blueprints.accBasic())
.alias('acc')
.withChildren(
SBlueprint.of(Contact.class)
.set('LastName', 'TestContact')
.alias('childContact')
)
.withChildren(
SBlueprint.of(Opportunity.class)
.set('Name', 'TestOpportunity')
.set('StageName', 'Prospecting')
.set('CloseDate', Date.today().addDays(30))
.alias('childOpp')
);
Contact and Opportunity are each generated under the same Account with the parent Id copied into their respective AccountId. You can also use .times(...) on the child side, so uneven structures like "3 Contacts and 2 Opportunities under one Account" express naturally.
Multiplication: Upper-Tier times Propagates Downward
A crucial behavior to keep in mind when combining withChildren with .times(n): adding .times(n) at an upper tier re-generates the entire child subtree per parent, so record counts multiply.
Two Tiers: Parent N × Child M
SBlueprint.of(Account.class)
.set('Name', 'Acc-{#}')
.alias('acc_{#}')
.times(2) // 2 parents
.withChildren(
SBlueprint.of(Contact.class)
.parentIdField('AccountId')
.set('LastName', 'Con-{#}')
.alias('con_{#}')
.times(2) // 2 children per parent
);
Result: 2 Accounts and 2 × 2 = 4 Contacts.
Three Tiers and Beyond: Exponential Growth
Adding more tiers compounds the count multiplicatively.
SBlueprint.of(Account.class)
.times(2) // 2 parents
.withChildren(
SBlueprint.of(Contact.class)
.parentIdField('AccountId')
.times(2) // 2 children per parent → 4 total
.withChildren(
SBlueprint.of(Case.class)
.parentIdField('ContactId')
.times(2) // 2 grandchildren per child → 8 total
)
);
Total record count: 2 Accounts + 4 Contacts + 2 × 2 × 2 = 8 Cases. Remembering "the leaf count = the product of .times(...) along the hierarchy" makes it easy to anticipate counts.
Be careful: deeper hierarchies make record counts grow exponentially. Stacking five tiers with
.times(3)each generates3⁵ = 243records, eating into the DML row governor limit (10,000). "Just a few more" can balloon into a massive count, so be deliberate about each tier's.times(...).
times + {#}: Bulk Generation with Sequence Numbers
.times(n) produces n copies of the same blueprint. For cases like "3 Contacts" or "10 Accounts", you can do it in one line without a for loop.
The {#} placeholder is embedded inside .set(...) values or the argument to .alias(...). When expanded by times(n), {#} is replaced with 1, 2, 3, ....
SBlueprint.of(Contact.class)
.set('LastName', 'Contact-{#}')
.alias('con_{#}')
.times(3);
What gets generated:
| alias | LastName |
|---|---|
con_1 | Contact-1 |
con_2 | Contact-2 |
con_3 | Contact-3 |
Alphabetic Sequences: {A} / {a}
In addition to the numeric {#}, alphabetic sequence placeholders {A} / {a} are supported. {A} expands to 'A' / 'B' / 'C' ..., and {a} expands to 'a' / 'b' / 'c' ....
SBlueprint.of(Account.class)
.set('Name', 'Acc-{A}')
.alias('acc_{a}')
.times(3);
What gets generated:
| alias | Name |
|---|---|
acc_a | Acc-A |
acc_b | Acc-B |
acc_c | Acc-C |
Use these for test data where you want a human-readable distinction (e.g. labels appearing in test reports), where letters read more naturally than numbers.
startAt / interval: Changing the Start and Step
.set(...) / .alias(...) have extra arguments to specify the start and step for {#}.
SBlueprint.of(Account.class)
.set('Name', 'Acc-{#}', 10, 2) // 10, 12, 14
.alias('acc_{#}', 10, 2) // acc_10, acc_12, acc_14
.times(3);
This handles finer-grained needs like "start the sequence from N" or "only even-numbered indices".
use with Offsets: Linking Only Some Bulked Parents to Children
Sometimes you want to link only some of the bulked parent records to children. For example, "out of 10 Accounts, only attach Contacts to the latter 5 (6 to 10)".
use has 4- and 5-argument overloads that let you specify the start number and step of the alias being referenced.
Signature: .use(String alias, String fromField, String toField, Integer startAt [, Integer interval])
SOrchestrator.start()
.add(
SBlueprint.of(Account.class)
.alias('acc_{#}')
.template(Blueprints.accBasic())
.times(10) // acc_1 to acc_10
)
.add(
SBlueprint.of(Contact.class)
.set('LastName', 'Con-{#}')
.alias('con_{#}', 6) // con_6 to con_10
.use('acc_{#}', 'Id', 'AccountId', 6) // reference acc_6 to acc_10
.times(5)
);
Five children — con_6 through con_10 — are created, each with acc_6 through acc_10 as their parent. This overload shines when you need to express uneven mappings, not "every child has the same parent".
{P0} / {P1} / ...: Referencing Upper-Tier Values
Why This Placeholder Is Needed
In nestings that use .times(...) at each tier, "the true parent" as seen from a leaf record is a different instance each time it's generated. For example:
- Parent (Account)
.times(2) - Child (Contact)
.times(2) - Grandchild (Case)
.times(2)
In this case, there are 8 grandchildren, 4 children (2 per parent), and 2 parents. From each grandchild's perspective, "its true parent (Contact)" is one specific Contact out of the 4.
However, if you attach a simple {#} alias on the Contact side like .alias('child_{#}'), "the child_1 under Acme-1" and "the child_1 under Acme-2" end up declaring the same alias twice, which raises a duplicate-alias error and halts execution.
There is a workaround. By embedding the parent's alias to form a compound alias like .alias('{P0}_child_{#}'), you get four unique aliases: __Account_0_1___child_1 / __Account_0_1___child_2 / __Account_0_2___child_1 / __Account_0_2___child_2, and grandchildren can address them via .use('__Account_0_1___child_1', ...).
But this approach has drawbacks:
- The grandchild's
.use(...)side has to mentally assemble the alias string of "its true parent" - If you change the hierarchy later, the alias string assembly logic has to be rewritten everywhere
- As a result, the test code reads like an "alias-string puzzle" rather than a "data structure"
The {P0} / {P1} / {P2} ... placeholders avoid this structurally. SOrchestrator hierarchically resolves "the true parent for me, at the Nth tier from the top (0-indexed)" — so you don't have to attach aliases, and you don't have to mentally assemble alias strings.
How to Count: Absolute Depth from the Root
The counting is not "distance upward from me" — it's absolute depth from the root:
{P0}: the root (outermost parent, tier 0){P1}: one level below the root (tier 1){P2}: another level below (tier 2)- And so on, increasing as you go deeper
So when you want to reference "tier 1's value from a fourth-tier record", you specify {P1} (not "three steps up from where I am").
Minimal Example: Using {P0} in Two Tiers
Let's start with the simplest two-tier case, referencing {P0} (= the root). Here the child Contact's Description simply receives the parent Account's Name.
SBlueprint.of(Account.class) // P0 (root)
.set('Name', 'Acme')
.withChildren(
SBlueprint.of(Contact.class)
.set('LastName', 'TestContact')
.use('{P0}', 'Name', 'Description') // copy root Account's Name into Description
);
The Contact's Description becomes 'Acme'. {P0} points to "the root Account", and you don't need to attach a .alias(...) on the parent side.
This is enough to understand the basic pattern of "create one parent and copy its value into the child". More advanced usage — deeper hierarchies with .times(...) involved — is covered in the next example.
Example: A Grandchild Copies Its True Parent's Value
In a three-tier setup (parent / child / grandchild), the grandchild Case's Subject receives the LastName of its own true parent Contact.
SBlueprint.of(Account.class) // P0 (tier 0 / root)
.set('Name', 'Acme-{#}')
.times(2)
.withChildren(
SBlueprint.of(Contact.class) // P1 (tier 1)
.parentIdField('AccountId')
.set('LastName', 'Contact-{#}')
.times(2)
.withChildren(
SBlueprint.of(Case.class) // P2 (tier 2 = grandchild Case)
.parentIdField('ContactId')
.use('{P1}', 'LastName', 'Subject') // ← the LastName of its true parent Contact
.times(2)
)
);
The records and the grandchild Case Subjects (= the LastName of each grandchild's true parent) are as follows:
| Account | Contact (true parent) | Case (grandchild) Subject |
|---|---|---|
Acme-1 | Contact-1 (under Acme-1) | Contact-1 |
Acme-1 | Contact-2 (under Acme-1) | Contact-2 |
Acme-2 | Contact-1 (under Acme-2) | Contact-1 |
Acme-2 | Contact-2 (under Acme-2) | Contact-2 |
Each Contact has 2 grandchildren below it, so there are 8 grandchild Cases — the four combinations above, two each. There are two Contact records with the same Contact-1 LastName (one under Acme-1, one under Acme-2), but each grandchild receives the value from the real parent it actually hangs under.
Points:
- Without attaching any aliases, "the true parent for me" is hierarchically resolved automatically
- The counting is the absolute depth from the root (not the distance upward from your current position)
- Let
withChildren's automatic processing handle parent Id copying — only use{Pn}when you specifically need to copy some other field value
parentIdField: Disambiguating When the Child Has Multiple Lookups
If a child object has multiple lookup candidates (e.g. Contact has both AccountId and CustomAccount__c), withChildren alone can't decide "which lookup to put the parent Id into". You disambiguate with .parentIdField(...).
Signature: .parentIdField(String fieldName)
SBlueprint.of(Account.class)
.alias('acc')
.withChildren(
SBlueprint.of(Contact.class)
.parentIdField('AccountId') // specify which lookup gets the parent Id
.template(Blueprints.conBasic())
);
Objects without multiple lookups don't need .parentIdField(...). Think of it as "add it when the ambiguity error fires" — a safety net you reach for after the fact.
sharedWith: Sharing Follows Bulk Generation (v2.0.0+)
.sharedWith(user, accessLevel) declares a manual share as a final state. You never assemble Foo__Share / AccountShare rows yourself, and you never think about inserting them after the parent.
Signature: .sharedWith(User user, String accessLevel) (accessLevel is 'Read' or 'Edit')
What matters in the context of this page is that a share declaration rides the same bulk machinery described above. Parents multiplied with times get the same number of shares.
SBlueprint.of(Invoice__c.class)
.set('Name', 'Invoice-{#}')
.alias('inv_{#}')
.owner(admin)
.sharedWith(rep, 'Read')
.times(5);
// → 5 Invoice__c records, and 5 matching Invoice__Share records
A share declared on a nested child behaves the same way — it multiplies along with the parent's times.
SBlueprint.of(Account.class)
.set('Name', 'Acme-{#}')
.times(2)
.withChildren(
SBlueprint.of(Invoice__c.class)
.owner(admin)
.sharedWith(rep, 'Read')
.times(3) // 6 invoices → 6 shares
);
None of this is special-cased for sharing. Internally sharedWith assembles a sibling blueprint wired to the parent with use(), so times propagation and {Pn} resolution take exactly the same path as any ordinary child (see How Dependency Resolution Works).
A contradictory declaration fails before the DML
Shares are not something you can always write, so declarations that cannot hold raise ApexBlueprintException during the analysis stage of create().
| Situation | Why |
|---|---|
The object's org-wide default is Public | Manual share rows don't exist at all (Foo__Share is not found) |
Sharing with the very user given to owner() | The owner already has full access, and Salesforce rejects the manual share |
accessLevel is anything other than 'Read' / 'Edit' | Bad argument |
None of these reach the DML. Instead of "I inserted it and the share wasn't there", you get a reasoned failure at declaration time.
📌 For how to build the persona on the other side of the share, see the SPersona API reference.
Example: One Parent + Three Children + One Grandchild
Bringing everything together into a single test:
@isTest
static void testAccountWithContactsAndCase() {
SOrchestrator orchestrator = SOrchestrator.start()
.add(
SBlueprint.of(Account.class)
.template(Blueprints.accBasic())
.set('Name', 'ParentAccount')
.alias('acc')
.withChildren(
SBlueprint.of(Contact.class)
.set('LastName', 'Contact-{#}')
.use('{P0}', 'Name', 'Description') // root Account (P0)'s Name into Description
.alias('con_{#}')
.times(3)
)
)
.add(
SBlueprint.of(Case.class)
.set('Subject', 'TestCase')
.use('con_2', 'Id', 'ContactId') // link to the 2nd Contact
.alias('targetCase')
);
orchestrator.create();
Account parent = (Account) orchestrator.getByAlias('acc');
Contact con2 = (Contact) orchestrator.getByAlias('con_2');
Case targetCase = (Case) orchestrator.getByAlias('targetCase');
Assert.areEqual(3, [SELECT COUNT() FROM Contact WHERE AccountId = :parent.Id]);
Assert.areEqual('ParentAccount', con2.Description);
Assert.areEqual(con2.Id, targetCase.ContactId);
}
Points:
- The nested
withChildrenvisualizes "3 Contacts hang under the Account" directly use('{P0}', 'Name', 'Description')copies the parent Account's Name into each child Contact's Description- A
{#}-bearing alias (con_{#}) lets you later retrieve the 2nd Contact as'con_2'and link a Case to it - The test body contains no complex procedure — "the final data structure I want" reads directly from the code
Example: A Shared Reference Across Trees (Diamond Dependency)
withChildren expresses a tree, but real integration tests often need a deeply nested child to reference a shared record that sits outside the tree. Take a four-tier Account → Opportunity → Quote → QuoteLineItem chain where the deepest QuoteLineItem references a shared Product2: the dependency is no longer a tree but a diamond (a DAG).
Declare the parent-child chain with withChildren, the cross-tree reference with use, and register the shared record with its own add so an alias can reach it. The add order is free — SOrchestrator's topological sort works out that Product2 and Account must precede QuoteLineItem.
@isTest
static void testQuoteLineItemReferencesSharedProduct() {
SOrchestrator orchestrator = SOrchestrator.start()
.add(
SBlueprint.of(Product2.class)
.set('Name', 'Widget')
.alias('product') // shared record, outside the tree
)
.add(
SBlueprint.of(Account.class)
.template(Blueprints.accBasic())
.withChildren(
SBlueprint.of(Opportunity.class)
.template(Blueprints.oppBasic())
.withChildren(
SBlueprint.of(Quote.class)
.set('Name', 'Q-2026')
.withChildren(
SBlueprint.of(QuoteLineItem.class)
.template(Blueprints.qliBasic())
.set('Quantity', 3)
.use('product', 'Id', 'Product2Id') // cross-tree reference
.alias('qli')
)
)
)
);
orchestrator.create();
Product2 product = (Product2) orchestrator.getByAlias('product');
QuoteLineItem qli = (QuoteLineItem) orchestrator.getByAlias('qli');
Assert.areEqual(product.Id, qli.Product2Id);
}
Key points:
- The shared record (
Product2) is not a branch of the tree, so it gets its ownadd+aliasrather thanwithChildren - The cross-tree reference is one line:
use('product', 'Id', 'Product2Id'). Combined with thewithChildrenchain, the dependency graph becomes a diamond rather than a tree - Write
addcalls in whatever order reads best. Insert order is resolved by SOrchestrator's topological sort, so it never concerns you - Required fields on
QuoteLineItem(PricebookEntryId,UnitPrice, and in practice aPricebookEntrysupplied through its ownadd) belong in theBlueprints.qliBasic()template, leaving only the delta under test (Quantity) and the reference (use) in the test body
Related Documents
- Declaring a Single Record with SBlueprint: The five core methods
- Resolving Dependencies and Executing DML with SOrchestrator: The execution engine
- API Reference: SBlueprint: Full method signatures
- Back to the ApexBlueprint Guide