assets/config_template.json
[
{
"ConfigurationName": "OutboundMessages",
"Label": "<OBM DeveloperName exactly as in the SOP>",
"Fields": {
"EndpointUrl": "<literal post-refresh URL from the SOP>",
"Object": "<target SObject, e.g. Account / Contact / Asset>"
},
"IsActive": true,
"ExecutionOrder": 1
},
{
"ConfigurationName": "RemoteSiteSettings",
"Label": "<Remote Site name exactly as in the SOP>",
"Fields": {
"RemoteSiteUrl": "<literal post-refresh URL from the SOP>"
},
"IsActive": true,
"ExecutionOrder": 2
},
{
"ConfigurationName": "ScheduledApex",
"Label": "<Human-readable job name from the SOP — typically the Setup > Scheduled Jobs display name>",
"Fields": {
"ApexClassName": "<Bare Apex identifier of the Schedulable class, e.g. NightlyDataSyncScheduler>",
"CronExpression": "<6- or 7-field Salesforce cron expression, e.g. 0 0 2 * * ?>",
"JobName": "<Setup > Scheduled Jobs display name, must equal CronJobDetail.Name post-schedule>"
},
"IsActive": true,
"ExecutionOrder": 3
},
{
"ConfigurationName": "Flows",
"Label": "<Human-readable description, e.g. \"Deactivate Nightly Sync Flow\">",
"Fields": {
"FlowApiName": "<Flow DeveloperName — unique across Flows, e.g. Nightly_Sync_Flow>",
"Action": "Deactivate"
},
"IsActive": true,
"ExecutionOrder": 4
}
]
assets/json_schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Post-Copy Automation Config",
"description": "Array of declarative post-copy actions consumed by the Salesforce post-copy automation tool.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"ConfigurationName",
"Label",
"Fields",
"IsActive",
"ExecutionOrder"
],
"allOf": [
{
"if": {
"properties": { "ConfigurationName": { "const": "RemoteSiteSettings" } }
},
"then": {
"properties": {
"Fields": {
"type": "object",
"additionalProperties": false,
"required": ["RemoteSiteUrl"],
"properties": {
"RemoteSiteUrl": {
"description": "The exact API field name for a Remote Site Setting URL. Must be spelled RemoteSiteUrl — Url, RemoteSiteURL, SiteUrl, and EndpointUrl are all invalid.",
"type": "string",
"minLength": 1
}
}
}
}
}
},
{
"if": {
"properties": { "ConfigurationName": { "const": "OutboundMessages" } }
},
"then": {
"properties": {
"Fields": {
"type": "object",
"required": ["Object"],
"anyOf": [
{ "type": "object", "required": ["EndpointUrl"] },
{ "type": "object", "required": ["Action"] }
]
}
}
}
},
{
"if": {
"properties": { "ConfigurationName": { "const": "ScheduledApex" } }
},
"then": {
"properties": {
"Fields": {
"type": "object",
"additionalProperties": false,
"required": ["ApexClassName", "CronExpression", "JobName"],
"properties": {
"ApexClassName": {
"description": "Bare Apex identifier of the class that implements Schedulable. Must match ^[A-Za-z_][A-Za-z0-9_]*$.",
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
},
"CronExpression": {
"description": "Salesforce cron expression — 6 or 7 whitespace-separated fields.",
"type": "string",
"minLength": 1
},
"JobName": {
"description": "Human-readable job name. Appears in Setup > Scheduled Jobs and equals CronTrigger.CronJobDetail.Name post-schedule.",
"type": "string",
"minLength": 1
}
}
}
}
}
},
{
"if": {
"properties": { "ConfigurationName": { "const": "Flows" } }
},
"then": {
"properties": {
"Fields": {
"type": "object",
"additionalProperties": false,
"required": ["FlowApiName", "Action"],
"properties": {
"FlowApiName": {
"description": "Flow DeveloperName — unique across Flows in the org. Maps to FlowDefinition.DeveloperName. Must match ^[A-Za-z_][A-Za-z0-9_]*$.",
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
},
"Action": {
"description": "Whether the apply-side skill should Activate or Deactivate the flow. Exact string match, case-sensitive.",
"type": "string",
"enum": ["Activate", "Deactivate"]
},
"VersionNumber": {
"description": "Optional. Only meaningful when Action is Activate — the specific flow version to activate. If omitted for Activate, the apply-side skill resolves to the latest saved version. Ignored for Deactivate.",
"type": "integer",
"minimum": 1
}
}
}
}
}
}
],
"properties": {
"ConfigurationName": {
"description": "Salesforce configuration / metadata type. Must be one of the values listed in references/configuration_catalog.md.",
"type": "string",
"enum": [
"OutboundMessages",
"RemoteSiteSettings",
"ScheduledApex",
"Flows"
]
},
"Label": {
"description": "The metadata Label or DeveloperName of the specific record being updated, exactly as it appears in the SOP.",
"type": "string",
"minLength": 1
},
"Fields": {
"description": "Map of API field name -> concrete value taken verbatim from the SOP. Do not include fields that are duplicated by Label (RemoteSiteName) or by the top-level IsActive flag.",
"type": "object",
"minProperties": 1,
"additionalProperties": {
"type": ["string", "number", "boolean"]
},
"$comment": "Per-ConfigurationName key constraints are enforced by the allOf/if-then blocks above."
},
"IsActive": {
"description": "Whether the post-copy tool should execute this action. Default true.",
"type": "boolean"
},
"ExecutionOrder": {
"description": "Phase number. Entries with no dependency on each other share the same value and run in parallel; lower numbers run before higher.",
"type": "integer",
"minimum": 1
}
}
}
}
examples/sample_sop_excerpt.md
# Sample SOP Excerpt
This is a representative slice of a customer SOP — the type of input the
skill receives. The matching expected output is in
`sample_sop_to_config.json`.
---
## Post Refresh Steps – Update Outbound Messages Endpoints
Update the Outbound Messaging Endpoints after the refresh. Navigate to
Setup > Process Automation > Workflow Actions > Outbound Messages.
| Outbound Message | Post-refresh endpoint URL |
|------------------------|-------------------------------------------------|
| `IR_Account_OBM_PROD` | `https://uat.example.com/services/account` |
| `IR_Contact_OBM_PROD` | `https://uat.example.com/services/contact` |
| `IR_Asset_OBM_PROD` | `https://uat.example.com/services/asset` |
The OBM names embed the target SObject (`Account`, `Contact`, `Asset`),
which becomes the mandatory `Object` field on each generated entry.
## Post Refresh Steps – Remote Site Settings
Update the Remote Site Settings under Setup > Security > Remote Site
Settings. Both should remain Active.
| Remote Site Name | Post-refresh URL |
|--------------------|----------------------------------------|
| `R12_Remote_Site` | `https://uat.example.com` |
| `Tavant_API` | `https://tavant.uat.example.com` |
examples/sample_sop_to_config.json
[
{
"ConfigurationName": "OutboundMessages",
"Label": "IR_Account_OBM_PROD",
"Fields": {
"EndpointUrl": "https://uat.example.com/services/account",
"Object": "Account"
},
"IsActive": true,
"ExecutionOrder": 1
},
{
"ConfigurationName": "OutboundMessages",
"Label": "IR_Contact_OBM_PROD",
"Fields": {
"EndpointUrl": "https://uat.example.com/services/contact",
"Object": "Contact"
},
"IsActive": true,
"ExecutionOrder": 1
},
{
"ConfigurationName": "OutboundMessages",
"Label": "IR_Asset_OBM_PROD",
"Fields": {
"EndpointUrl": "https://uat.example.com/services/asset",
"Object": "Asset"
},
"IsActive": true,
"ExecutionOrder": 1
},
{
"ConfigurationName": "RemoteSiteSettings",
"Label": "R12_Remote_Site",
"Fields": {
"RemoteSiteUrl": "https://uat.example.com"
},
"IsActive": true,
"ExecutionOrder": 2
},
{
"ConfigurationName": "RemoteSiteSettings",
"Label": "Tavant_API",
"Fields": {
"RemoteSiteUrl": "https://tavant.uat.example.com"
},
"IsActive": true,
"ExecutionOrder": 2
}
]
references/configuration_catalog.md
# Configuration Catalog
The fixed catalog of supported `ConfigurationName` values for the post-copy
automation config. **Never invent a new value** — if an SOP step does not
fit a row below, raise it to the user.
The catalog currently covers four types only:
1. `OutboundMessages`
2. `RemoteSiteSettings`
3. `ScheduledApex`
4. `Flows`
Anything else (Custom Labels, Connected Apps, Named Credentials, SSO, etc.)
is out of scope for this skill at present — surface them to the user and
recommend a follow-up to extend the catalog.
## How to use this catalog
For each post-copy action extracted from the SOP:
1. Find the row whose **SOP signal** phrases match the action.
2. Use the `ConfigurationName` from that row.
3. Use one of the listed **Field keys** in your entry's `Fields` object.
4. Put the **concrete value** from the SOP (literal URL, name, etc.) as
the value. If the SOP does not state a concrete value, **skip the
entry** entirely and list it in the response.
## Catalog
### OutboundMessages
- **SOP signal phrases**: "outbound message", "OBM", "endpoint URL",
"Workflow Actions > Outbound Messages".
- **Field keys** (both required for non-delete entries):
- `EndpointUrl` — value is the literal post-refresh URL the SOP gives
for that outbound message.
- `Object` — the SObject the outbound message targets (e.g., `Account`,
`Contact`, `Asset`, `Lead`). This is mandatory because the same
`Label` may apply to multiple OBMs that differ only by entity, and
the post-copy tool needs the Object to disambiguate. Infer from the
OBM name (e.g., `IR_Account_OBM_PROD` → `Account`,
`IR_Contact_OBM_PROD` → `Contact`) or from explicit text in the SOP.
If the entity is genuinely ambiguous and the SOP gives no signal,
skip the entry and surface it.
- **Skip when**: the SOP only names the outbound message but does not
give a new endpoint URL, OR the target Object cannot be determined.
- **Special action**: deletion. When the SOP says "delete all endpoints",
use `Fields: { "Action": "Delete", "Object": "<entity>" }`. (`"Delete"`
is a literal instruction the tool understands, not a placeholder.)
### RemoteSiteSettings
- **SOP signal phrases**: "remote site setting", "remote site", "trusted
URL", "Setup > Security > Remote Site Settings".
- **Field keys**:
- `RemoteSiteUrl` — value is the literal post-refresh URL. The key must
be spelled **exactly** `RemoteSiteUrl`. Do **not** use `Url`,
`RemoteSiteURL`, `SiteUrl`, or `EndpointUrl` (that last one is the OBM
key, not the remote-site key). The post-copy tool matches the field by
exact API name, so any other spelling silently no-ops at runtime.
- **Do NOT add**:
- `RemoteSiteName` — the name lives in the top-level `Label` field.
- `IsActive` — the active flag lives at the top level of the entry.
Putting either inside `Fields` duplicates information the tool already
has and creates ambiguity.
- **Skip when**: the SOP names the remote site but does not give a new
URL.
### ScheduledApex
- **SOP signal phrases**: "scheduled apex", "scheduled job", "cron
trigger", "System.schedule", "Setup > Scheduled Jobs", "re-schedule
the nightly job", "schedule the batch class after refresh".
- **Field keys** (all three required):
- `ApexClassName` — the bare Apex identifier of the class that
implements `Schedulable`. Must match `^[A-Za-z_][A-Za-z0-9_]*$` —
no dots, no spaces, no namespace prefix in this field, no `.cls`
suffix. Example: `NightlyDataSyncScheduler`.
- `CronExpression` — a 6- or 7-field Salesforce cron expression. The
apply-side skill validates the shape, but do not emit a
single-word / malformed value here. Example: `0 0 2 * * ?`
(every day at 2:00 AM).
- `JobName` — the human-readable job name that appears in Setup >
Scheduled Jobs. Post-schedule this equals
`CronTrigger.CronJobDetail.Name`; the apply-side skill uses it
both as the `System.schedule` argument and as the SOQL key when
verifying the job materialized. Typically the same string the
customer uses to refer to the job.
- **Do NOT add**:
- Any other Field key (e.g. `NextFireTime`, `State`, `CronTriggerId`)
— `CronTrigger` is read-only and computed by the platform, so only
the three inputs to `System.schedule` belong in `Fields`.
- **Skip when**: the SOP names a scheduled job but does not give all
three of `ApexClassName`, `CronExpression`, and `JobName`. Never
fabricate one from the others (for example, do not derive a JobName
from the class name or vice versa) — surface the incomplete entry
in the response so the user can supply the missing piece.
### Flows
- **SOP signal phrases**: "flow", "deactivate flow", "activate flow",
"turn off <FlowName>", "turn on <FlowName>", "Setup > Process Automation
> Flows", "post-copy flow deactivation", "reactivate flow after refresh".
- **Field keys**:
- `FlowApiName` (required) — the flow's DeveloperName, which is unique
across Flows in the org. Maps to `FlowDefinition.DeveloperName` in
the Tooling API. Do NOT use the flow label / MasterLabel; labels
are not unique. If the SOP names a flow by label only, look up the
API name within the SOP's own accompanying material (an endpoint
table, companion sheet, screenshot caption) before emitting the
entry. If the API name cannot be located from the supplied SOP
sources, **skip the entry and flag it in the summary** — never
guess or synthesize an API name from the label.
- `Action` (required) — exactly `"Activate"` or `"Deactivate"` (case-
sensitive). Verbatim string literal — the apply-side skill matches
by exact value.
- `VersionNumber` (optional, positive integer) — meaningful only when
`Action` is `"Activate"`. Selects which saved flow version to make
active. If omitted for `Activate`, the apply-side skill resolves to
the latest saved version. Ignored (and should be omitted) for
`Deactivate`.
- **Semantics**:
- `Action: "Deactivate"` deactivates the flow if it is currently
active. If it is already inactive, the apply-side skill records
`ALREADY_INACTIVE` and does not issue a PATCH.
- `Action: "Activate"` activates the requested version (or the
latest saved version if `VersionNumber` is omitted). If the flow
is already active at that version, the apply-side skill records
`ALREADY_ACTIVE` and does not issue a PATCH.
- **Do NOT add**:
- Any other Field key (e.g. `Status`, `ActiveVersionId`,
`activeVersionNumber`, `Label`). Flow activation/deactivation is
driven entirely by `Fields.FlowApiName` + `Fields.Action` (+
optional `Fields.VersionNumber`); the apply-side skill owns the
Tooling API mutation shape.
- **Skip when**: the SOP names a flow but does not indicate whether to
activate or deactivate it, OR the API name (DeveloperName) of the
flow cannot be determined from the SOP. Never fabricate a
`FlowApiName` from the label.
## Adding a new ConfigurationName
If a customer SOP introduces a configuration type not in the list above,
**stop and ask the user**. Adding a new value requires:
1. Update the `enum` in `assets/json_schema.json`.
2. Add a new section to this file.
3. Update `examples/sample_sop_to_config.json` if relevant.
4. Update the description in `SKILL.md` to include the new type.
references/sop_parsing_patterns.md
# SOP Parsing Patterns
Heuristics for turning SOP prose into post-copy action records.
## Identify the Post-Copy / Post-Refresh section
SOPs are typically structured as:
1. Pre-Refresh Steps (or Pre-Copy)
2. Refresh / Copy Steps (the act of cloning)
3. **Post-Refresh Steps** (or Post-Copy) — this is the section that
produces actions. Everything else is filtered out.
Section markers to look for (case-insensitive):
- `Post Refresh Steps` / `Post-Refresh Steps`
- `Post Copy Steps` / `Post-Copy Steps`
- `After Refresh`
- `After Copy`
If the SOP has no such section, ask the user which sections are post-copy.
## Action signal patterns
Each post-copy action in the JSON output corresponds to one SOP instruction
that **also includes a concrete value** (URL, etc.). If the value is
missing, skip the action.
### Pattern 1 — "Update X to Y" (Outbound Messages)
> "Update the OBM Messages endpoint for `IR_Account_OBM_PROD` to
> `https://uat.example.com/account`."
- One entry per outbound message named in the SOP.
- `ConfigurationName: "OutboundMessages"`,
`Fields: { "EndpointUrl": "<literal URL from SOP>", "Object": "<entity>" }`.
- `Label` = the outbound message Name as it appears in the SOP.
- `Object` is **mandatory** — infer from the OBM name
(`IR_Account_OBM_PROD` → `Account`, `IR_Contact_OBM_PROD` → `Contact`,
`IR_Asset_OBM_PROD` → `Asset`) or from explicit text in the SOP.
The same Label may apply to two OBMs that differ only by entity, so
the post-copy tool relies on `Object` to disambiguate.
If the SOP names the outbound message but does not state the new
endpoint URL ("Update the OBM endpoints" with no table or list of
URLs), **skip** and list the omission in the response. If the target
Object cannot be inferred from the OBM name or surrounding text,
also skip and surface it.
### Pattern 2 — Tabular updates with explicit URL columns
> A table with columns: `Remote Site Name`, `Prod URL`, `UAT URL`,
> `Difference found`.
- One entry per row whose `Difference found` (or equivalent) flag
indicates the URL changed.
- `ConfigurationName: "RemoteSiteSettings"`,
`Fields: { "RemoteSiteUrl": "<UAT URL from row>" }`.
- `Label` = the Remote Site Name from the row.
- Skip rows where Prod and UAT URLs are identical (no update needed).
- Do **not** add `RemoteSiteName` to `Fields` (lives in `Label`).
- Do **not** add `IsActive` to `Fields` (lives at the top level).
### Pattern 3 — "Delete all X"
> "Delete all integration endpoints in the OBM Messages setup."
- A single entry with `Fields: { "Action": "Delete" }` and a Label
describing the deletion target ("All OBM Endpoints" or similar).
`"Delete"` here is a literal instruction the tool understands.
### Pattern 4 — Sequential / phased instructions
> "First, update the OBM endpoints. Then, update the Remote Site Settings."
- Use the order in `ExecutionOrder`. See "Ordering heuristic" below.
### Pattern 5 — Conditional / environment-specific
> "If you are refreshing fcUAT, do X. For all other sandboxes, do Y."
- Emit both entries. Mark the conditional one with `IsActive: false` so
the customer can toggle it per-environment, and surface the
conditionality in your response message.
## Ordering heuristic — `ExecutionOrder` is a phase number
Treat `ExecutionOrder` like a phase / wave number, not a per-row index.
**Entries that have no dependency on each other share the same number.**
The post-copy tool runs all entries with the same number in parallel.
Default phasing (use unless the SOP explicitly says otherwise):
| Phase | Type | Why |
|-------|------|-----|
| 1 | OutboundMessages | Endpoint URLs the rest of the system depends on; no inter-OBM dependency |
| 2 | RemoteSiteSettings | Trusted URLs subsequent integrations need; no inter-site dependency |
Within a single `ConfigurationName`, every entry shares the same phase
number — there is no inter-row dependency among OBMs or among remote
sites. Across types, run OBMs before remote sites unless the SOP says
otherwise.
If the SOP states explicit dependencies ("Step 5 must finish before
Step 6"), break the dependent entry into a later phase.
## Locating values across multi-sheet sources
Customer SOPs often split the **action list** from the **endpoint
table**:
- The "Post Refresh / Post Copy" section names which OBMs / Remote
Sites need updating, but only by name.
- A separate sheet (or appendix) holds a Prod URL → UAT URL table.
Before skipping an entry for "missing value", search **every tab /
sheet of every file the user supplied** for a row keyed by the same
OBM / RemoteSite / Configuration name. Look for column headers like
`Prod Endpoint URL`, `UAT Endpoint URL`, `Post-refresh URL`, or
`Endpoint`. The post-refresh value is the one to emit.
If after searching every supplied source the value is still not
present, **skip the entry** and list it in the response. Never emit
empty strings, `null`, or placeholder markers (`<from-backup>`, `TBD`,
`TODO`) — customers consume the JSON directly and unpopulated fields
break the tool.
## Filter rules
Skip these — they are not post-copy automation actions:
- Manual ServiceNow ticket creation (process / ops, not metadata).
- "Email the SSO team" / human notifications.
- "Schedule a meeting" / calendar actions.
- "Click Save" / UI navigation breadcrumbs.
- Pre-refresh data collection ("copy the endpoints to a spreadsheet").
- Verification / test steps with no metadata write.
- Any action that targets a configuration type **not** in
`configuration_catalog.md` (Custom Labels, Connected Apps, Named
Credentials, SSO, etc.) — list these in your response message so the
user knows what was filtered and can prioritize extending the catalog.
- Any action where the SOP names the metadata but does not give a
concrete value (URL, etc.). List these too in the response.
If a section is mostly process and not configuration, note in your
response message that the section was skipped and why.
## Disambiguating Label vs. DeveloperName
When the SOP shows an Outbound Message or Remote Site name like
`R12_Remote_Site`, use that as the `Label` field — this is the
DeveloperName, which is what the post-copy tool resolves at runtime.
When the SOP shows a UI Label like "TK Marine Parts Store Online", that's
ambiguous. Ask the user or pick the most likely API name based on the
surrounding context (the SOP usually shows the API name in setup
breadcrumbs).
references/source_format_handling.md
# Source Format Handling
The customer SOP can arrive in any of the formats below — sometimes
several at once. This file gives the exact extraction recipe per
format so action records and endpoint values can be located reliably,
and explains how to decide whether an embedded image carries data or
is illustrative.
> **Rule of thumb**: the action list (which OBM / Remote Site to
> update) and the endpoint table (the actual URL values) are
> frequently in **different files or different tabs / pages**. Always
> read every source the user supplies before deciding a value is
> missing. If a value still cannot be located after exhausting every
> source, skip the entry — never emit empty / placeholder fields.
---
## PDF (`.pdf`)
```python
import pypdf
reader = pypdf.PdfReader(path)
for i, page in enumerate(reader.pages):
text = page.extract_text()
```
- Many enterprise SOPs are slide decks exported to PDF — the text
layer is usually present, but tables may flatten oddly. Inspect the
raw extracted text rather than trusting visual alignment.
- For section navigation, search case-insensitively for: "Post
Refresh", "Post-Refresh", "Post Copy", "Post-Copy", "After
Refresh", "Update <X>".
- If `extract_text()` returns empty or near-empty for a page, the
page is likely an embedded image. OCR it:
```python
import pdfplumber, pytesseract
with pdfplumber.open(path) as pdf:
for page in pdf.pages:
if not (page.extract_text() or "").strip():
img = page.to_image(resolution=200).original
text = pytesseract.image_to_string(img)
```
- If `pytesseract` is unavailable, fall back to `pdftotext -layout`
(Poppler) and `tesseract` CLI.
## xlsx (`.xlsx`)
```python
import openpyxl
wb = openpyxl.load_workbook(path, data_only=True)
for sname in wb.sheetnames:
ws = wb[sname]
for row in ws.iter_rows(values_only=False):
for cell in row:
...
```
- **Always read every sheet.** Customer planners routinely split the
task list and the URL table across tabs (real example: Michelin UAT
Refresh Planner — task list in `Integration (Indus)`, URL table in
`Evolution SFA` columns K/L/M).
- Iterate **every column** up to `ws.max_column`. Some sheets have
data far past column M (e.g., column index 10000+) that the visible
layout hides.
- Read with `data_only=True` so cached formula values are returned.
- Check `cell.comment` for free-text annotations that often contain
the post-refresh value.
- Look for canonical column headers: `Outbound Message`, `Remote
Site`, `Object`, `Endpoint`, `Endpoint URL`, `Prod Endpoint URL`,
`UAT Endpoint URL`, `Post-refresh URL`. The post-refresh value is
the one to emit.
- For embedded images on a sheet, use `ws._images` (or unzip the
xlsx and inspect `xl/media/`) — see the image rules below.
## csv (`.csv`)
```python
import csv
with open(path, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
...
```
- Treat the same way as a single xlsx sheet — look for the canonical
column headers above.
## JSON (`.json`)
- If the customer already supplies a JSON document, scan every key
for the canonical names (`outboundMessage`, `endpointUrl`, `object`,
`remoteSiteUrl`, `label`, `name`). Be tolerant of casing
(`endpointURL`, `EndpointUrl`, `endpoint_url`).
- Customer JSON might already be partially in the target shape; do
not blindly copy it through — re-validate every entry against
`assets/json_schema.json` and the catalog.
## docx (`.docx`)
```python
from docx import Document
doc = Document(path)
for p in doc.paragraphs:
...
for tbl in doc.tables:
for row in tbl.rows:
for cell in row.cells:
...
```
- Extract both paragraph text and table cells. Word tables
frequently hold the endpoint table.
- Inline images (`doc.inline_shapes`) follow the image rules below.
## Markdown / plain text (`.md`, `.txt`)
- Read directly with the `Read` tool. Tables in pipe-delimited
Markdown are easy to parse — split on `|`.
## Pasted excerpts in the conversation
- The user may paste content directly. Treat it as plain text.
- If they paste an image (drag-drop), use the `Read` tool on the
image path and follow the image rules below.
---
## Images (`.png`, `.jpg`, `.jpeg`, `.tiff`, `.bmp`)
Two kinds of image arrive in customer SOPs:
| Kind | Examples | What to do |
|------|----------|------------|
| **Data-bearing** | screenshot of the Outbound Messages list page showing real Names + Endpoint URLs; captured Remote Site Settings table with real URLs; spreadsheet snippet pasted as a screenshot; configuration page where the endpoint values are visible | OCR and treat the values as authoritative for the entries they cover |
| **Illustrative** | "this is what the setup screen looks like" with placeholder values; architecture diagram showing system topology; UI flow chart; redacted/blurred values; screenshot whose surrounding text says "for example" / "as shown below your values will look like this" | Do NOT extract values — the image is documentation, not data |
### Heuristic — is this image data-bearing?
Yes (use the values), if any of:
- The surrounding paragraph references it as the source of truth:
"the values listed below", "as captured in the spreadsheet
above", "copy these into the post-refresh setup", "the URLs you
copied before the refresh".
- The image shows a Salesforce setup table (Outbound Messages list,
Remote Site Settings list, Custom Metadata Type list) with real
values for known catalog types.
- Real endpoint URLs are visible (full domain, not redacted, not
obviously a placeholder like `https://example.com/...`).
- The image directly precedes or follows a "Post Refresh Steps"
heading and lists the entities named in that step.
No (skip the image), if any of:
- Surrounding text says "for example", "as a sample", "this is what
it will look like", "your real values will differ".
- The values shown are placeholders (`PROD_URL`, `<your domain>`,
`username/password`, all-`x` or all-`*` redaction).
- The image is a flowchart / arch diagram / sequence diagram.
- The image is a UI walkthrough screenshot whose purpose is to show
*where* to click, not *what* to enter.
- The image is a stock screenshot of the Salesforce Help / Setup
interface unrelated to the customer's actual values.
### OCR recipe
```python
from PIL import Image
import pytesseract
img = Image.open(image_path)
text = pytesseract.image_to_string(img)
```
For higher accuracy on tables:
```python
text = pytesseract.image_to_string(img, config="--psm 6")
```
`--psm 6` treats the image as a single uniform block of text (good
for tables). Try `--psm 4` for column-of-text layouts.
If `pytesseract` is unavailable, **read the image with the `Read`
tool and transcribe the visible values into a temporary text
buffer** — the multimodal `Read` reliably picks up table contents
in screenshots.
### After OCR
- Normalise whitespace, strip trailing zero-width chars.
- Fix common OCR confusions in URLs: `0`/`O`, `1`/`l`/`I`, `:`/`;`,
`/`/`\`, `_`/`—`. Cross-check with surrounding text where possible.
- If two URL columns are present (Prod / UAT), use the
**post-refresh** column.
- Treat OCR'd values like any other extracted value — if the text
is unreadable or ambiguous, skip the entry and surface it. Do
NOT guess to fill a field.
---
## Multi-source merge
When the user supplies multiple files:
1. Read every file end-to-end first; build an internal map of
"named entity → values found".
2. Identify the action list source (which OBMs / Remote Sites need
updating) — usually the file with "Post Refresh Steps".
3. For each named action, look up the value in the map. If found in
any source, use it. If not found anywhere, skip and surface.
4. Emit one entry per (configuration, label) pair — do not duplicate
when the same OBM appears in multiple sources.
---
## Failure modes to surface in the response
When you skip an entry, list it explicitly so the customer can
correct the source. Use one of these labels in the response:
- **Missing value** — the entity is named but no concrete value was
found in any supplied source.
- **Out of catalog** — the action targets a configuration type not
yet supported (Custom Labels, Connected Apps, etc.).
- **Unreadable image** — an image was data-bearing but OCR returned
unintelligible output.
- **Ambiguous Object (OutboundMessages only)** — the OBM name does
not reveal the target SObject and no supplemental table provides it.
SKILL.md
---
name: automation-sandbox-post-copy-config-generate
description: "Generates the JSON config file that the Salesforce sandbox post-copy automation tool consumes, from a customer SOP in any format (PDF, xlsx, csv, JSON, docx, Markdown, plain text, or a screenshot of an endpoint table). Use when the user asks to create, build, generate, produce, or convert a post-copy or post-refresh automation config — turning a sandbox-refresh SOP into a JSON array of OutboundMessages (OBM), RemoteSiteSettings, ScheduledApex, and Flows entries with ConfigurationName, Label, Fields, IsActive, and ExecutionOrder. Also trigger for phrasings like \"post-copy config\", \"post-refresh automation JSON\", \"update the outbound message (OBM) endpoints after refresh\", \"convert this SOP to config\", \"remote site settings JSON\", \"refresh planner to JSON\", or \"sandbox refresh config\". DO NOT TRIGGER when: user wants to deploy the generated config to an org (use platform-metadata-deploy), or apply/execute/run/dry-run the post-copy automation JSON against a sandbox (use automation-sandbox-post-copy-configure)."
metadata:
relatedSkills:
- "automation-sandbox-post-copy-configure"
- "platform-metadata-deploy"
version: "1.2"
domains: ["Automation"]
---
# Automation: Sandbox Post-Copy Config Generate
Convert a customer's sandbox-refresh / post-copy SOP into a structured JSON
array that the post-copy automation tool consumes. Each entry is a
declarative instruction: which Salesforce configuration to update, which
fields are involved, whether it is active, and what order it runs in.
## STOP — do this before writing any JSON
Do **not** compose the output from memory. Before you write the file, you
MUST open and read `assets/config_template.json` and copy an entry from it
for each action. Every output entry is exactly one of these four shapes —
five top-level keys, no others, no wrapper object:
```json
[
{
"ConfigurationName": "OutboundMessages",
"Label": "IR_Account_OBM_PROD",
"Fields": { "EndpointUrl": "https://uat.example.com/services/account", "Object": "Account" },
"IsActive": true,
"ExecutionOrder": 1
},
{
"ConfigurationName": "RemoteSiteSettings",
"Label": "R12_Remote_Site",
"Fields": { "RemoteSiteUrl": "https://uat.example.com" },
"IsActive": true,
"ExecutionOrder": 2
},
{
"ConfigurationName": "ScheduledApex",
"Label": "Nightly Data Sync",
"Fields": { "ApexClassName": "NightlyDataSyncScheduler", "CronExpression": "0 0 2 * * ?", "JobName": "Nightly Data Sync" },
"IsActive": true,
"ExecutionOrder": 3
},
{
"ConfigurationName": "Flows",
"Label": "Deactivate Nightly Sync Flow",
"Fields": { "FlowApiName": "Nightly_Sync_Flow", "Action": "Deactivate" },
"IsActive": true,
"ExecutionOrder": 4
}
]
```
- `ConfigurationName`: exactly `OutboundMessages`, `RemoteSiteSettings`, `ScheduledApex`, or `Flows` — these are the ONLY four valid values and they are **plural / PascalCase** (never singular `OutboundMessage`, never `Type` / `Name` / `Operation`).
- Field keys inside `Fields` are **PascalCase** verbatim: `EndpointUrl`, `Object`, `RemoteSiteUrl`, `ApexClassName`, `CronExpression`, `JobName`, `FlowApiName`, `Action`, `VersionNumber` — never lowercase (`endpointUrl`), never duplicated at the top level (no `isActive`/`Label` inside `Fields` — those already live at the top level of the entry).
- OBM `Fields`: `EndpointUrl` + `Object` (both required). RemoteSite `Fields`: `RemoteSiteUrl` only — never `Url`/`RemoteSiteURL`. ScheduledApex `Fields`: `ApexClassName` + `CronExpression` + `JobName` (all three required). Flows `Fields`: `FlowApiName` + `Action` (both required); `Action` is exactly `"Activate"` or `"Deactivate"`; optional `VersionNumber` (positive integer) is meaningful only for `Activate`.
- `ExecutionOrder` is a **phase number**, not a per-row counter. The `1, 2, 3, 4` in the template above only differ because each example is a different entry type. **Multiple entries of the same type with no dependency on each other share the same number** — e.g., three independent OBMs all get `ExecutionOrder: 1`.
- Top level is a JSON array. No `steps`/`actions`/`records` wrapper. No `<…>` or `REPLACE_WITH_…` placeholder ever survives into the output.
If you announce "I will now write …" without having read the template and
catalog, stop and read them first — a from-memory guess produces the wrong
keys and fails at runtime.
## Scope
- **In scope**: Reading a customer SOP in any of the supported formats
(PDF, xlsx, csv, JSON, docx, Markdown, plain text, pasted excerpt, or
images containing data tables — e.g., a screenshot of an
Outbound Messages list with endpoint URLs), identifying post-copy /
post-refresh actions, mapping each action to a supported
`ConfigurationName`, emitting the canonical JSON array.
- **Out of scope**: Generating Salesforce metadata XML (delegate to
`generating-*` skills), deploying anything to an org, running the
post-copy tool, inferring or fabricating values not present in the SOP —
if the SOP does not give a concrete URL/value for an action, skip the
action.
**Every emitted entry must have every Field populated with a real value
from the customer source.** No empty strings, no `null`, no
`<from-backup>` / `TBD` / `TODO` placeholders. Customers should never
see an unpopulated field in the output — if a value cannot be located,
skip the entry and surface it. See the corresponding rule below.
---
## Required Inputs
Gather or infer before generating:
- **SOP source(s)**: One or more paths (or pasted content) in any of:
PDF, xlsx, csv, JSON, docx, Markdown, plain text, or images
(.png/.jpg/.jpeg/.tiff/.bmp). Multiple files are common — the action
list and the endpoint table sometimes live in different files. Read
every file the user supplies.
- **Target output path**: Where the JSON config should be written. Default
to `post-copy-config.json` in the current directory unless specified.
- **Scope filter** (optional): If the SOP covers many environments
(e.g., fcQA, fcUAT, multiple sandboxes), confirm which subset the user
wants in the output.
If the user provides a clear SOP and target, generate immediately without
asking unnecessary questions.
---
## Workflow
All steps are sequential. Steps 1–5 (reading the SOP, the catalog, the
template, and the schema) are **prerequisites to writing** — you may not
skip to the write step. If you catch yourself about to emit JSON without
having read `assets/config_template.json` and
`references/configuration_catalog.md`, go back and read them first.
1. **Locate and read every supplied SOP source** — read
`references/source_format_handling.md` for the exact extraction
recipe per format (PDF, xlsx, csv, JSON, docx, image). At a glance:
- **PDF**: extract text with `pypdf` (text layer) and OCR
image-based pages with `pytesseract` if the text layer is empty.
- **xlsx**: read every sheet with `openpyxl` (`data_only=True`),
scan all columns including ones outside the visible default
range, check cell comments and embedded media.
- **csv / JSON / Markdown / text**: read directly.
- **docx**: extract paragraphs and tables with `python-docx`.
- **Images** (.png/.jpg/...): use the `Read` tool to view, then
decide if the image carries data (a table of endpoint URLs, a
setup screenshot showing values to capture) or is purely
illustrative (architecture diagram, flow chart). Extract values
only from data-bearing images. See the image-handling rules in
`references/source_format_handling.md`.
- For very large SOPs (>50 pages / >20 sheets), focus on sections
or sheets titled "Post Refresh", "Post-Copy", "Post-Refresh
Steps", "Update …", or equivalent.
2. **Identify post-copy actions** — read
`references/sop_parsing_patterns.md` for the heuristics that turn prose
instructions ("Update Outbound Message endpoint X to URL Y") into
structured action records.
3. **Map each action to a `ConfigurationName`** — load
`references/configuration_catalog.md`. The catalog currently supports
`OutboundMessages`, `RemoteSiteSettings`, `ScheduledApex`, and
`Flows`. Any action that targets a different configuration type is
out of scope: skip it and list it in the response so the user can
extend the catalog later.
4. **Read the JSON template** — load `assets/config_template.json`. It
shows the exact required shape of one `OutboundMessages` entry, one
`RemoteSiteSettings` entry, one `ScheduledApex` entry, and one
`Flows` entry, with `<…>` placeholder slots. Copy an entry, replace every `<…>` slot
with the concrete SOP value, and keep the exact top-level keys
(`ConfigurationName`, `Label`, `Fields`, `IsActive`, `ExecutionOrder`)
— never rename them to `Type`, `Name`, `Operation`, etc. Never emit
an entry that still contains a `<…>` placeholder; if you cannot fill
a slot, skip the entry (see Rules).
5. **Validate against the schema** — load `assets/json_schema.json`. Every
entry must conform: `ConfigurationName` is one of the catalog values,
`Fields` is an object, `IsActive` is boolean, `ExecutionOrder` is a
positive integer.
6. **Group entries by phase, then assign `ExecutionOrder`** —
`ExecutionOrder` is a phase number, not a per-row counter. Entries that
can run in parallel (no dependency between them) share the same value.
Different `ConfigurationName` types typically get different phases; all
entries within one phase share its number. See the ordering heuristic
in `references/sop_parsing_patterns.md`.
7. **Compare against the example** — verify the output shape against
`examples/sample_sop_to_config.json` before writing.
8. **Write the JSON file** — emit pretty-printed JSON (2-space indent).
---
## Rules / Constraints
| Constraint | Rationale |
|-----------|-----------|
| Output is a JSON array at the top level (not an object with a wrapper key) | The post-copy tool consumes an array directly |
| Every entry has all five required keys: `ConfigurationName`, `Label`, `Fields`, `IsActive`, `ExecutionOrder` | The tool fails fast on missing keys; partial entries are not silently accepted |
| `ConfigurationName` is one of the catalog values | Unknown values cause the runtime mapper to error out — never invent a new type without updating the catalog |
| `Fields` keys are the actual API field names on the target metadata | Wrong key names mean the tool can't locate the field at runtime |
| `Fields` values are the concrete values from the SOP (literal URLs, names, etc.) | The post-copy tool applies the value as-is; placeholders are not resolved at runtime |
| If the SOP names an action but no concrete value (URL, etc.) is provided, **skip the entry** entirely and list it in the response | Generating an entry without a real value would produce a silent no-op or a deployment error at runtime |
| Every Field in every emitted entry must be a real value sourced from the customer's SOP or a supplemental sheet they provided. Never emit `""`, `null`, or placeholder markers like `<from-backup>` / `TBD` / `TODO` | Customers consume the JSON directly — empty / placeholder fields surface to them as broken output and would also fail at runtime |
| Before skipping an OBM / RemoteSite for missing values, **search every tab / sheet of the supplied workbook (and every supplied file)** for an endpoint table keyed by that name | Customer SOPs frequently split the action list and the URL table across different sheets (e.g., the Michelin UAT Refresh Planner lists OBMs in the Integration tab but the URL table lives in the Evolution SFA tab) |
| Information already captured by another field is **not** repeated in `Fields` (e.g., `RemoteSiteName` lives in `Label`, `IsActive` lives at the top level — neither belongs in `Fields`) | Duplicate keys make the entry ambiguous and waste bytes the tool then has to reconcile |
| For `OutboundMessages`, `Fields` MUST include both `EndpointUrl` and `Object` (the target SObject — `Account`, `Contact`, `Asset`, `Lead`, etc.) | Same Label can apply to multiple OBMs differing only by entity; `Object` disambiguates them at runtime |
| For `RemoteSiteSettings`, the URL key MUST be spelled exactly `RemoteSiteUrl` — never `Url`, `RemoteSiteURL`, `SiteUrl`, or `EndpointUrl` | The post-copy tool matches the field by exact API name; any other spelling means it can't locate the field and the entry silently no-ops at runtime |
| For `Flows`, `Fields` MUST include `FlowApiName` (the flow's DeveloperName — unique across Flows) and `Action` (exactly `"Activate"` or `"Deactivate"`, case-sensitive). `Fields.VersionNumber` (positive integer) is optional and only meaningful when `Action` is `"Activate"`; if omitted for Activate, the apply-side skill resolves to the latest saved version. | `FlowApiName` is the unique lookup key on `FlowDefinition.DeveloperName`; `Action` is a fixed instruction verb (not a Salesforce field name) that the apply-side skill matches by exact string. Flow labels are not unique — never use them as the lookup. |
| `ExecutionOrder` is a phase number — entries with no dependency on each other share the same value | The post-copy tool runs all entries with the same `ExecutionOrder` in parallel; sequencing is only needed where one action depends on another |
| `IsActive: false` entries remain in the output (do not delete them) | The customer toggles them on per-environment; deleting loses traceability |
| Default `IsActive` to `true` when the SOP marks an action as required | Most SOP steps are required; explicit opt-out is the exception |
### Canonical entry shape
See the entry shapes shown in the STOP section above, or copy directly from
`assets/config_template.json`. Do **not** rename the five top-level keys
(`ConfigurationName`, `Label`, `Fields`, `IsActive`, `ExecutionOrder`) to
`Type`, `Name`, `Operation`, `apiName`, etc., and do **not** wrap the array
in an object with a `steps` / `actions` / `records` key.
---
## Gotchas
| Issue | Resolution |
|-------|------------|
| SOP step says "delete all endpoints" rather than "update X to Y" | Emit an entry with `ConfigurationName: "OutboundMessages"`, `Label` describing the deletion target, and a note in `Fields` (`{"Action": "Delete"}`); the catalog documents the Delete pattern |
| Same Label appears for multiple environments (fcQA + fcUAT) | Emit one entry per environment; suffix the Label or use the environment-specific Label as it appears in the SOP |
| For OutboundMessages, the same Label can legitimately apply to multiple entries when each targets a different `Object` (e.g., one OBM for Account, one for Contact). | Do not collapse them — emit one entry per Object. The `Object` value (inferred from the OBM name like `IR_Account_OBM_PROD` → `Account`) is what distinguishes them, not the Label. |
| SOP groups many Custom Labels together in a single table | One entry per row of the table — do not collapse into a single bulk entry |
| Action targets a setting outside the catalog (CustomLabels, ConnectedApps, NamedCredentials, SSO, CustomSettings, etc.) | Skip the action, do NOT invent a new `ConfigurationName`; list every skipped action in the response so the user knows what to add to the catalog later |
| SOP includes pre-refresh steps interleaved with post-refresh | Filter out pre-refresh — only post-refresh / post-copy actions belong in the output |
| SOP names an outbound message / remote site to update but does not include the new URL | Skip the entry. List the skipped item in the response so the user can supply the URL or amend the SOP |
| SOP includes secrets in URLs (e.g., `https://USER:TOKEN@host/...`) | Embed them verbatim — the tool consumes the value as-is — but flag in the response so the user is aware the JSON now contains a secret and should be stored / shared accordingly |
| User's SOP is in a non-English language or has heavy formatting (tables, callouts) | Extract the prose first, normalize whitespace, then parse — formatting artifacts do not need to survive into the JSON |
| The SOP includes screenshots / images. Some are illustrative (architecture diagrams, flow charts, "this is what the setup screen looks like" examples); others are data-bearing (a screenshot of a real Outbound Messages list, a captured Remote Site table). | Treat illustrative images as out-of-scope — do NOT extract values from them. For data-bearing images (the surrounding text refers to "the values shown below" / "as captured in the screenshot above" / a real Endpoint URL is visible), OCR them and use the values. See `references/source_format_handling.md` for the heuristic. |
| The SOP is supplied as multiple files (e.g., a PDF + a companion xlsx + a screenshot folder), or values are split across tabs / pages | Read every supplied file end-to-end before skipping any entry. The action list and the URL table are frequently in different files — see the rule above about multi-sheet sources. |
---
## Output Expectations
Deliverables:
- A single JSON file (default `post-copy-config.json`) containing a
top-level array of post-copy action entries.
- A short summary in your response listing: total entries, count per
`ConfigurationName`, actions skipped because they did not map to the
catalog, and actions skipped because the SOP did not include a concrete
value.
The output file structure conforms to `assets/json_schema.json`.
---
## Cross-Skill Integration
| Need | Delegate to |
|------|-------------|
| Generate the actual Salesforce metadata XML for a Custom Label / Named Credential / Remote Site / etc. | The matching `generating-*` skill |
| Deploy generated metadata or run the post-copy tool against an org | `deploying-metadata` skill |
| Compare an SOP against entities defined in UDD | `udd:research-entities` |
---
## Reference File Index
| File | When to read |
|------|-------------|
| `assets/config_template.json` | Step 4 — starting structure for the JSON output |
| `assets/json_schema.json` | Step 5 — validate every emitted entry |
| `references/configuration_catalog.md` | Step 3 — to map an SOP action to a `ConfigurationName` and get the canonical Field keys |
| `references/sop_parsing_patterns.md` | Step 2 and step 6 — heuristics for extracting actions from prose and ordering them |
| `references/source_format_handling.md` | Step 1 — exact extraction recipe per input format (PDF / xlsx / csv / JSON / docx / image) and the data-bearing-vs-illustrative image heuristic |
| `examples/sample_sop_to_config.json` | Step 7 — verify output shape matches expected format |
| `examples/sample_sop_excerpt.md` | Step 7 — see a representative SOP excerpt that produced the sample JSON |