Bojan Bojan
Ask AI
I am the Great and Powerful Oz, keeper of Bojan's secrets and his 298 five-star reviews. Step forward and ask, traveler, or tell me who you are and I shall tailor the spectacle.
I'm hiringI have a projectJust curiousAI & platforms
powered by cloudflare workers ai · llama
← All writing
SampleHQNov 18, 20254 min read

The Real Story: How SampleHQ Automates Salesforce Metadata from WordPress

Most Salesforce integrations brag about “syncing contacts” or “pushing deals.” Cute. Basic. The easy part. The hard part-the part nobody publicly writes about-is this: Deploying Visualforce pages Injecting Quick Actions Editing Page Layout XML Handling Enterprise vs Professional vs Platform limitations Doing all of the above without touching the Salesforce UI And doing it from

Most Salesforce integrations brag about “syncing contacts” or “pushing deals.”
Cute. Basic. The easy part.

The hard part-the part nobody publicly writes about-is this:

This is the side of Salesforce that usually requires a “team of consultants”.
I didn’t want a team. I wanted a pipeline.

So SampleHQ treats Salesforce metadata the same way you treat code:

Versioned → generated → deployed → tested → re-deployed → repeatable.

What follows is the exact, real code that makes all of this work.


1. Visualforce Page Creation – Fully Automated

The goal was simple:

If the VF page doesn’t exist, create it.
If it exists, skip it.
And never touch Salesforce manually.

Here’s the core block from Installer.php:

private static function ensure_visualforce_page(SalesforceClient $client, string $instance_url, string $access_token): array {
    $page_name = 'SampleHQ_Picker';
    $soql = sprintf(
        "SELECT Id, Name FROM ApexPage WHERE Name = '%s' LIMIT 1",
        addslashes($page_name)
    );

    $query = self::salesforce_tooling_query($instance_url, $access_token, $soql);
    if (!empty($query['data']['records'])) {
        return [
            'ok' => true,
            'id' => (string) ($query['data']['records'][0]['Id'] ?? ''),
            'name' => $page_name,
            'created' => false,
        ];
    }

    $metadata_payload = [
        'fullName' => $page_name,
        'label' => 'SampleHQ Picker',
        'markup' => self::build_visualforce_markup(),
        'apiVersion' => self::get_salesforce_api_version_number(),
        'availableInTouch' => false,
        'confirmationTokenRequired' => false,
    ];

    $response = $client->metadataCreate('ApexPage', $metadata_payload);
    if (!$response['ok']) {
        return ['ok' => false, 'error' => 'Failed to create Visualforce page: ' . ($response['error'] ?? 'Unknown')];
    }

    $created_lookup = self::salesforce_tooling_query($instance_url, $access_token, $soql);
    $records = $created_lookup['data']['records'] ?? [];

    return [
        'ok' => !empty($records),
        'id' => $records[0]['Id'] ?? '',
        'name' => $page_name,
        'created' => true,
    ];
}

The part I love is this:
The entire Visualforce markup is generated in PHP – not stored in Salesforce.

Edit your VF UI → commit → deploy.
If Salesforce rejects it, you see the raw error.

No more “Open Dev Console and try again.”


2. Quick Action Creation – Metadata, Not Clicking

The second piece of the pipeline:
Create the “Link SampleHQ Orders” Quick Action automatically.

The relevant block:

private static function ensure_quick_action(
    SalesforceClient $client,
    string $instance_url,
    string $access_token,
    string $page_name
): array {

    $developer_name = 'SampleHQ_LinkSampleOrders';
    $label = 'Link SampleHQ Orders';

    $soql = sprintf(
        "SELECT Id FROM QuickActionDefinition WHERE DeveloperName = '%s' LIMIT 1",
        addslashes($developer_name)
    );

    // Already exists? Done.
    $query = self::salesforce_tooling_query($instance_url, $access_token, $soql);
    if (!empty($query['data']['records'])) {
        return [
            'ok' => true,
            'id' => (string) ($query['data']['records'][0]['Id'] ?? ''),
            'developer_name' => $developer_name,
            'label' => $label,
            'created' => false,
        ];
    }

    // Deploy via Metadata API
    $deployer = new MetadataDeployer();
    $deploy_result = $deployer->deployQuickAction(
        $access_token,
        $instance_url,
        $developer_name,
        $label,
        $page_name
    );

The XML looks like something Salesforce consultants would charge $10K to “configure”:

<QuickAction xmlns="http://soap.sforce.com/2006/04/metadata">
    <label>Link SampleHQ Orders</label>
    <type>VisualforcePage</type>
    <page>SampleHQ_Picker</page>
    <optionsCreateFeedItem>false</optionsCreateFeedItem>
    <height>400</height>
    <width>600</width>
</QuickAction>

But here – it’s just a string in PHP.

You edit it the same way you edit your theme.
Deploy it using code.
Rollback with Git.

This is how Salesforce should work.


3. Layout Automation – The Part That Feels Illegal

Salesforce layouts are encrypted inside SOAP responses, base64-encoded, zipped, and stored under paths that sometimes get URL-encoded for no reason.

So here’s what SampleHQ does:

This is the part where most engineers give up.
But this is the part that makes the whole system bulletproof.

Here’s the high-level method:

public function addQuickActionToLayout(
    string $access_token,
    string $instance_url,
    string $layout_full_name,
    string $quick_action_full_name
): array {
    $retrieve = $this->retrieveLayout($access_token, $instance_url, $layout_full_name);
    if (!$retrieve['ok']) return $retrieve;

    $modified = $this->addQuickActionToLayoutXml($retrieve['xml'], $quick_action_full_name);
    if ($modified === null) return ['ok' => false, 'error' => 'Failed to modify Layout XML'];

    return $this->deployLayout($access_token, $instance_url, $layout_full_name, $modified);
}

The SOAP retrieval:

'<m:retrieve><m:retrieveRequest><m:apiVersion>65.0</m:apiVersion><m:singlePackage>true</m:singlePackage>'

Polling:

'<m:checkRetrieveStatus><m:asyncProcessId>%s</m:asyncProcessId></m:checkRetrieveStatus>'

The extraction even handles Salesforce’s bizarre filename encoding:

private function normalizeLayoutSignature(string $layout_full_name): string {
    return strtolower(preg_replace('/[^a-zA-Z0-9]/', '', rawurldecode($layout_full_name)));
}

And the XML mutation:

$existing = $xpath->query(
    sprintf('//md:quickActionListItems/md:quickActionName[text()="%s"]',
        htmlspecialchars($quick_action_full_name))
);
if ($existing->length > 0) return $layout_xml;

If it doesn’t exist, we append:

<quickActionListItems>
    <quickActionName>Opportunity.SampleHQ_LinkSampleOrders</quickActionName>
</quickActionListItems>

Then redeploy the ZIP and poll the metadata API until Salesforce approves it.

This is layout-as-code.
No dragging buttons ever again.


Why This Matters

Because this is how you:

And most importantly:

Because when everything is code-driven, the entire installer can run inside a scratch org during automated tests.

Backed by the Salesforce CLI:

sf org create scratch -f config/scratch-platform.json
sf org create scratch -f config/scratch-enterprise.json
sf org create scratch -f config/scratch-professional.json

Backed by MCP:

This is what CRM integration looks like when you stop thinking “plugin” and start thinking “infrastructure.”

Related writing
Jun 13, 2026

From Zapier to AI Agents: The Four Levels of Business Automation

Jun 03, 2026

Every WordPress Error Page Beyond 404 Is Broken. I Built a Plugin to Fix Them.

May 29, 2026

Your Git Log Is a Legal Document