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:
- 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 a WordPress plugin
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:
- Retrieve layout via SOAP
- Poll until Salesforce returns a ZIP
- Search for the correct
.layoutfile (even if it’s renamed) - Parse it as XML
- Check if the Quick Action is already present
- If not, inject the XML node
- Build a new ZIP
- Deploy it back to Salesforce
- Poll until success
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:
- handle Enterprise
- handle Professional
- handle Platform
- handle orgs that can’t deploy metadata
- handle Salesforce at scale
- avoid 30-page setup docs
- avoid 3 Zoom calls with a Salesforce admin
- avoid “we forgot to add the button again”
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:
- spin up an org
- run the installer
- scrape results
- compare layout XML
- validate quick actions
- rebuild the VF page
- clean up
This is what CRM integration looks like when you stop thinking “plugin” and start thinking “infrastructure.”
Bojan