Skip to content

How to write a pool seed script

This guide writes a seedScript — the hook org pool-prepare runs against each freshly created pool org before marking it available. What a seed hook is for, and how a later claim uses what it did, is in what a seed hook is for and how a pool works.

Prerequisites

  • A pool definition to attach the script to — see maintain the pool.
  • A DevHub made pooling-ready (npm run org:pool:activate -- --target-org <devhub>).

Create the module

  1. Add a file under config/scratch-org-setup/, named for the pool it seeds:

    config/scratch-org-setup/<tag>-pool-seed.mjs
    
  2. Export a single async seed(ctx) function. ctx gives you:

    • targetOrg — the freshly created org's username.
    • devHubConnection — an authenticated @salesforce/core Connection to the DevHub.
    • targetOrgConnection — an authenticated Connection to the org being seeded.
    • run(file, args) — spawns node <file> ...args as a child process and returns the child. Route anything that pushes metadata, runs setup Apex or shells out to the CLI through it, never a direct import (why).

Pre-do claim-time work

Run the CLI's own initialization pipeline against the fresh org, so a later claim pays only for the difference:

// config/scratch-org-setup/ci-pool-seed.mjs
// Full scratch-org initialization via the CLI's reuse path — no data import, CI runs bring
// their own test data. Consumer-owned; invoked by `org pool-prepare` per pool-def `seedScript`.

export async function seed({ targetOrg, run }) {
    await run("scripts/dia-scripts/index.mjs", [
        "org",
        "scratch-create",
        "--reuse",
        "--target-org",
        targetOrg,
        "--no-prompt",
        "--do-not-open-org",
        "--skip-data-import",
    ]);
}

Do org work claim-time init never does

Assign a permission set that a scratch-definition features entry provisioned — a plain API call, so it runs in-process without run:

export async function seed({ targetOrgConnection }) {
    const [user] = (
        await targetOrgConnection.query(
            `SELECT Id FROM User WHERE Username = '${targetOrgConnection.getUsername()}'`,
        )
    ).records;
    const [permSet] = (
        await targetOrgConnection.query(
            "SELECT Id FROM PermissionSet WHERE Name = 'FeatureProvisionedAccess'",
        )
    ).records;

    await targetOrgConnection.sobject("PermissionSetAssignment").create({
        AssigneeId: user.Id,
        PermissionSetId: permSet.Id,
    });
}

Deploy metadata outside sfdx-project.json's packageDirectories — a CLI invocation, so through run:

export async function seed({ targetOrg, run }) {
    await run("config/scratch-org-setup/deploy-side-metadata.mjs", [
        "--target-org",
        targetOrg,
    ]);
}

Wire it into the pool definition

Point the pool's seedScript key at the new file (repo-root-relative):

{
    "tag": "ci",
    "maxAllocation": 10,
    "seedScript": "config/scratch-org-setup/ci-pool-seed.mjs"
}

Loading the definition fails fast if the file does not exist; every key is in the pool definition file reference.

Test it

Prepare a small pool and watch the seed run:

npm run org:pool:prepare -- --devhubusername devhub --tag ci --max 1

Each seeding's output is tagged with the org username, since parallel seedings interleave on stderr. A throwing seed() fails only that org's preparation; succeedOnError in the pool definition decides whether the run continues with the rest of the batch.