ApexBlueprint: Relations, bulk generation, and reference patterns

Apex Stem Docs
Apex StemApexBlueprintwithChildrentimesRelations
Move beyond single-record declarations with withChildren (nested parent-child), times + {#} placeholders (bulk with sequences), use offsets (partial parent references), {P0} / {P1} (parent / grandparent references), and parentIdField (lookup disambiguation).

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 blueprint
  • times(n) + {#} placeholder: bulk generation with sequence numbers
  • use with offsets: linking only some bulked parents to children
  • {P0} / {P1}: referencing ancestors (immediate parent / grandparent) inside nesting
  • parentIdField: disambiguating which lookup to fill when the child has multiple parent lookups
  • sharedWith: 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

APEX
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 AccountId is automatic. There's no need to write .use(...)
  • Defining the child blueprint inline inside withChildren visualizes the structure of "this parent has this child hanging under it"
  • If you attach .alias(...) on the child side, you can retrieve it via getByAlias('childContact')

1:N Parent-Child (Combined with times)

Using .times(n) inside withChildren lets you hang multiple children under the same parent.

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

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

APEX
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

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

APEX
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 generates 3⁵ = 243 records, 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, ....

APEX
SBlueprint.of(Contact.class)
    .set('LastName', 'Contact-{#}')
    .alias('con_{#}')
    .times(3);

What gets generated:

aliasLastName
con_1Contact-1
con_2Contact-2
con_3Contact-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' ....

APEX
SBlueprint.of(Account.class)
    .set('Name', 'Acc-{A}')
    .alias('acc_{a}')
    .times(3);

What gets generated:

aliasName
acc_aAcc-A
acc_bAcc-B
acc_cAcc-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 {#}.

APEX
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])

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

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

APEX
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:

AccountContact (true parent)Case (grandchild) Subject
Acme-1Contact-1 (under Acme-1)Contact-1
Acme-1Contact-2 (under Acme-1)Contact-2
Acme-2Contact-1 (under Acme-2)Contact-1
Acme-2Contact-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)

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

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

APEX
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().

SituationWhy
The object's org-wide default is PublicManual 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:

APEX
@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 withChildren visualizes "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.

APEX
@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 own add + alias rather than withChildren
  • The cross-tree reference is one line: use('product', 'Id', 'Product2Id'). Combined with the withChildren chain, the dependency graph becomes a diamond rather than a tree
  • Write add calls 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 a PricebookEntry supplied through its own add) belong in the Blueprints.qliBasic() template, leaving only the delta under test (Quantity) and the reference (use) in the test body