ApexBlueprint: Dependency resolution and DML execution with SOrchestrator

Apex Stem Docs
Apex StemApexBlueprintSOrchestratorTest DataDML
Execute blueprints with the four SOrchestrator methods (start / add / create / getByAlias). Covers automatic topological sort for insertion order, retrieving records by alias, and common pitfalls such as circular dependencies and alias duplication.

SOrchestrator is the engine that turns blueprints assembled by SBlueprint into real records. It automatically handles the parent → child insertion order, copying parent Ids into children, and resolving values referenced via aliases.

This page walks through the four methods of SOrchestrator (start / add / create / getByAlias) in order, and ends with a list of common pitfalls in real-world usage.

SOrchestrator.start(): Initialize the Builder

Every operation begins with this static method. It returns an empty SOrchestrator instance; you .add(...) blueprints to it, then call .create() to execute.

Signature: SOrchestrator.start()

APEX
SOrchestrator orchestrator = SOrchestrator.start();

.add(blueprint): Register a Blueprint in the Queue

.add(...) registers a single SBlueprint into SOrchestrator's queue.

Signature: .add(SBlueprint blueprint)

APEX
SOrchestrator.start()
    .add(
        SBlueprint.of(Account.class)
            .template(Blueprints.accBasic())
            .alias('parentAccount')
    )
    .add(
        SBlueprint.of(Opportunity.class)
            .set('Name', 'Test Opportunity')
            .set('StageName', 'Prospecting')
            .set('CloseDate', Date.today().addDays(30))
            .use('parentAccount', 'Id', 'AccountId')
            .alias('targetOpp')
    );

Behavior Notes

  • Addition order is ignored. SOrchestrator analyzes the dependencies internally and re-derives the insertion order via topological sort
  • It is fine to write the child before the parent. You can write blueprints in "the most readable order"
  • Multiple .add(...) calls can be chained on the same SOrchestrator
  • If you express parent-child relationships within a single blueprint via withChildren, registering the parent alone is enough — children come along with it (see Relations, Bulk Generation, and Reference Patterns)

.create(): Resolve Dependencies and Run DML

.create() analyzes the registered blueprints and executes the DML inserts in the correct order. By the time this call returns, every record is in the database and Ids have been issued.

Signature: .create()

APEX
SOrchestrator orchestrator = SOrchestrator.start()
    .add(/* ... */)
    .add(/* ... */);
orchestrator.create();

Behavior Notes

  • Internally runs a topological sort and inserts the depended-upon side (parents) first
  • Id references like .use('alias', 'Id', 'AccountId') copy the parent's Id (issued right after its insert) into the child's field before inserting the child
  • DML failures throw the usual Apex DML exceptions
  • The return value is void. You cannot append .create() to the .add(...) chain and assign it to a variable. Capture the SOrchestrator in a variable first, call orchestrator.create(), then call getByAlias(...) on that same variable

.getByAlias(name): Retrieve a Created Record

After create(), you can retrieve the created records by alias. You can bring "the Account you just created" to your hand without writing a SOQL query, which keeps assertion code short.

Signature: .getByAlias(String aliasName) (returns SObject)

APEX
SOrchestrator orchestrator = SOrchestrator.start()
    .add(
        SBlueprint.of(Account.class)
            .template(Blueprints.accBasic())
            .alias('parentAccount')
    );
orchestrator.create();
 
Account parent = (Account) orchestrator.getByAlias('parentAccount');
Assert.isNotNull(parent.Id);

Behavior Notes

  • The return value is SObject, so the caller casts it to the desired SObject type
  • Passing a non-existent alias returns null (not an exception). Typos are easy to miss, so guard with Assert.isNotNull(...) right after retrieval as a safe default
  • Records bulked via .times(n) with a '{#}' placeholder alias (e.g. 'con_{#}') can be retrieved individually via the expanded aliases like 'con_1' / 'con_2' / ... (see Relations, Bulk Generation, and Reference Patterns)

Example: A Complete Test Workflow

Combining startaddcreategetByAlias into a single test yields:

APEX
@isTest
static void testOppCreationWithAccount() {
    SOrchestrator orchestrator = SOrchestrator.start()
        .add(
            SBlueprint.of(Account.class)
                .template(Blueprints.accBasic())
                .alias('parentAccount')
        )
        .add(
            SBlueprint.of(Opportunity.class)
                .set('Name', 'Test Opportunity')
                .set('StageName', 'Prospecting')
                .set('CloseDate', Date.today().addDays(30))
                .use('parentAccount', 'Id', 'AccountId')
                .alias('targetOpp')
        );
    orchestrator.create();
 
    Account parent = (Account) orchestrator.getByAlias('parentAccount');
    Opportunity opp = (Opportunity) orchestrator.getByAlias('targetOpp');
 
    Assert.areEqual(parent.Id, opp.AccountId);
    Assert.areEqual('Test Opportunity', opp.Name);
}

Points:

  • Not a single SOQL in the test body. Assertion targets are retrieved directly via getByAlias(...)
  • We .add(...) the Account first here, but if we instead .add(...) the Opportunity first, the result would be identical — SOrchestrator re-orders things via dependency analysis
  • The parent Id is copied into the child by the .use('parentAccount', 'Id', 'AccountId') declaration alone. The procedure of "insert the parent, grab its Id into a variable, assign it to the child's AccountId..." disappears

Common Pitfalls

Circular Dependencies

If you create a dependency where both A.use('B', ...) and B.use('A', ...) hold, the topological sort cannot resolve it and .create() fails with Circular or invalid reference detected. Reconsider the design — either make one side a one-way reference, or replace the parent-child link with withChildren.

Duplicate Aliases

If .alias('foo') appears in two places within the same SOrchestrator, you'll hit Duplicate alias detected. When bulking via .times(n), declare aliases with a placeholder like 'foo_{#}' so that unique aliases are issued after expansion.

Reference to a Non-Existent Alias

If you reference an alias that hasn't been declared anywhere — e.g. .use('typoAlias', ...) — the call fails at .create() with the same family of errors (Circular or invalid reference detected). Alias typos are easy to miss, so it pays to develop the habit of reviewing "the first argument of .use" alongside "its corresponding .alias" as a pair.

Don't Rely on Auto-Generated Aliases for Retrieval

Omitting .alias(...) is fine for registering a blueprint; internally it gets an auto-generated alias like __Account_0_1__. However, retrieving these via getByAlias later is impractical. If you plan to retrieve a blueprint via getByAlias, always attach an explicit .alias(...) as a rule.