Bojan Josifoski < wp developer />

The Real Story: How SampleHQ Automates Salesforce Metadata from WordPress

November 18, 2025 • Bojan

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

About the Author

About the Author

I’m Bojan Josifoski - I’m a WordPress systems engineer who developed and maintained a proprietary WordPress-based framework used by U.S. financial institutions between 2016 and 2025.

← Back to Blog