Deployment actions (beta)
Deployment actions (beta)
This feature is currently in beta, but you can start using it right away.
What are deployment actions ?
Salesforce Deployments are mainly Metadata, but can also be other actions that will be performed before or after Metadata deployment:
- Run Apex scripts
- Upsert records (Import SFDMU project)
- Run command lines
- Publish Experience Cloud sites
- Schedule Apex batch jobs
- Remove items from package.xml before deployment
- Manual actions that cannot be automated
You can define them at two levels:
- Pull Request level: these actions will be run during the deployment of a feature pull request, but also during deployment of Pull Requests between major branches (ex:
preprodtomain) - Project level: The deployment actions will be performed during each deployment
Use DevOps Pipeline UI
You can display / create / edit deployment actions using My Pull Request button in the DevOps pipeline UI.
You can also see deployment actions of already merged Pull Requests by clicking on a major git branch name in the pipeline.
How to define deployment actions
Actions can be defined in properties commandsPreDeploy / commandsPostDeploy of .sfdx-hardis.yml config files.
- Pull Request level:
scripts/actions/.sfdx-hardis.<PR_ID>.yml(ex:scripts/actions/.sfdx-hardis.372.yml). - Repository level:
config/.sfdx-hardis.yml
Example of a Pull Request level configuration file defining pre-deploy and post-deploy actions:
# scripts/actions/.sfdx-hardis.372.yml
commandsPreDeploy:
- id: runInitApex
label: Run initialization apex
type: apex
parameters:
apexScript: scripts/apex/init.apex
context: process-deployment-only
- id: removeKnowledgeFlag
label: Remove KnowledgeUser flag
type: command
command: >-
sf data update record --sobject User --where "UserPermissionsKnowledgeUser='true'" --values "UserPermissionsKnowledgeUser='false'" --json
context: all
commandsPostDeploy:
- id: importTemplates
label: Import email templates
type: data
parameters:
sfdmuProject: EmailTemplate
context: process-deployment-only
- id: publishSite
label: Publish Experience site
type: publish-community
parameters:
communityName: "My Experience Site"
context: process-deployment-only
Configuration: action object shape
Each action is an object with the following required and optional properties.
| Field | Type | Required? | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier for the action. |
label |
string | Yes | Human-readable description of the action. |
type |
string | Yes | One of command, data, apex, publish-community, schedule-batch, remove-packagexml-items, manual. |
context |
string | Yes | When the action should run. Allowed values: all (default), check-deployment-only, process-deployment-only. |
command |
string | No | Shell command to run (used by command type). |
parameters |
object | No | Parameters of the action (see action details) |
customUsername |
string | No | Run the action with a specific username instead of the default target org. |
allowFailure |
boolean | No | If true and the action fails, the deployment continues but the result is marked failed/allowed. |
runOnlyOnceByOrg |
boolean | No | Default: true. If true, the action runs only once per target org. Execution state is tracked in a dedicated "Deployment Actions" PR comment (see below) - no Salesforce custom object required. |
Post-deployment actions are never run when the metadata deployment failed. They are reported as
not runin the Pull Request comment, no execution state is stored for them, and they are proposed again during the next successful deployment.
Which Pull Requests are in scope
The actions collected for a deployment depend on the branch the merged Pull Request comes from.
| Merge | Scope |
|---|---|
From a feature branch (ex: feature/my-story -> integration) |
Only the Pull Request that has just been merged |
Between major branches (ex: integration -> uat) |
Every Pull Request merged since the previous merge |
From a retrofit branch (ex: retrofit/from-main -> integration) |
Every Pull Request merged since the previous merge |
A feature branch merge carries a single Pull Request, so its notification and its Pull Request comment list only that Pull Request's actions. A major branch or retrofit branch merge carries a batch of Pull Requests, whose actions must be replayed in the target org.
- Between major branches, the batch is every Pull Request merged into the source major branch since its last promotion.
- Into the production branch (which has no promotion target), the batch is every Pull Request carried by the go-live merge itself.
- Pull Requests merged into upstream branches are part of the batch as soon as their commits arrive in the window: a hotfix merged into
mainis collected when a retrofit branch brings it down tointegration, so its actions run there too.
In every case, runOnlyOnceByOrg state tracking ensures each action runs only in the orgs where it has not been performed yet.
The same scope applies to the Apex test classes selected from Pull Requests when enableDeploymentApexTestClasses is active.
The resolved scope is visible in two places:
- In job logs, as a single line:
Pull Request scope: 5 Pull Request(s) (#4491, #4494, ...). Each Running/Skipping line then shows the Pull Request that defines the action. - In the check and deployment Pull Request comments, which state which Pull Requests the deployment actions and Apex test classes were collected from, with links.
If the deployment job of a feature branch fails, its actions are not picked up by the next merged Pull Request. Re-run the failed deployment job, or open a new Pull Request carrying the actions of the previous one.
Deployment Actions PR comment
After every action runs, sfdx-hardis creates or updates a dedicated "Deployment Actions" comment on the Pull Request. This gives release managers a consolidated view of what has been executed across every org for the lifetime of the PR.
Comment structure - one shared comment per PR, across all CI workflows:
- A Pending manual actions checklist: one checkbox per manual action still waiting to be performed in an org. Tick a box once you have performed the action: the next sfdx-hardis job records it as done (see Confirming manual actions with checkboxes).
- A Status by org branch matrix: one row per action, one column per org branch, so you can see at a glance in which orgs an action has been performed and where it is still pending.
- A collapsible Action Details section with the action properties (type, context, command or script...) and truncated output per org.
Example of the status matrix:
### Status by org branch
| Action | When | integration | uat |
|-----------------------------|-------------|:------------------------------:|:-----------------------------:|
| Remove KnowledgeUser flag | pre-deploy | ✅ 2024-06-01<br/>[12345](...) | ✅ 2024-06-05<br/>[12890](...) |
| Import email templates | post-deploy | ✅ 2024-06-01<br/>[12345](...) | ✅ 2024-06-05<br/>[12890](...) |
| Publish Experience site | post-deploy | ❌ 2024-06-02<br/>[12501](...) | ⬜ |
| Check external callback URL | post-deploy | 👋 2024-06-01<br/>[12345](...) | ⬜ |
*Legend: ✅ done · ❌ failed · 👋 waiting for manual execution · ⚪ skipped · ⬜ not run in this org branch yet*
Columns are ordered from dev to production (integration → uat → preprod → prod), rows follow the deployment order (pre-deploy actions first, then post-deploy). Each cell shows the status icon, the execution date and a link to the CI job that performed the action. A Last updated date is displayed under the matrix. The action id is embedded in each row as an HTML comment for machine parsing.
Status icons:
| Icon | Status | Meaning |
|---|---|---|
| ✅ | success |
Executed successfully (or confirmed as done via its checkbox) |
| ❌ | failed |
Executed but failed - will be retried next run |
| 👋 | manual |
Manual step - waiting for a human to perform it and tick the box |
| ⚪ | skipped |
Skipped (e.g. already run via runOnlyOnceByOrg) |
| ⬜ | - | Not run in this org branch yet |
Comments written with the previous format (one row per action and org branch pair) are still parsed, and are migrated to the matrix format on their next update.
runOnlyOnceByOrg - skip-on-next-run logic
When runOnlyOnceByOrg is true (the default), the "Deployment Actions" PR comment is used as the state store:
- If the table already contains a ✅
successrow for(actionId, orgBranch), the action is skipped with a ⚪ status on subsequent deployments. - ❌
failedentries are always retried on the next run. - Each action is tracked per org independently: the same action will run once in
integrationand once inuat.
Requirements for runOnlyOnceByOrg:
- A git provider token must be configured (GitHub:
GITHUB_TOKEN, GitLab:CI_SFDX_HARDIS_GITLAB_TOKEN, Azure DevOps:SYSTEM_ACCESSTOKEN, Bitbucket:CI_SFDX_HARDIS_BITBUCKET_TOKEN). - Without a git provider, actions with
runOnlyOnceByOrg: trueare skipped with a warning (to avoid untracked re-executions). All other actions still run normally; only the PR comment update is skipped.
Opt out: Add runOnlyOnceByOrg: false explicitly on any action that should always run.
Confirming manual actions with checkboxes
Manual action checklists appear in three kinds of Pull Request comments: check results, deployment results, and the Deployment Actions comment. Every checklist item carries a hidden marker identifying the action and the org branch.
When someone ticks one of these checkboxes (in any of the three comments), the next check or deployment job:
- records the action as done for that org branch in the Deployment Actions comment,
- skips it in later deployments to that org (same behavior as a successful
runOnlyOnceByOrgaction), - ticks the same checkbox in the other comments where the action appears, so all views stay consistent.
This works on GitHub, GitLab, Azure DevOps and Bitbucket, and requires the same git provider token as runOnlyOnceByOrg state tracking.
Action implementations
| Action type | Purpose |
|---|---|
command |
Run an arbitrary shell or sf command. |
data |
Import a SFDMU project. |
apex |
Run an Apex script file through the local sf apex integration. |
publish-community |
Publish an Experience Cloud (community) site. |
schedule-batch |
Schedule an Apex batch job with a cron expression. |
remove-packagexml-items |
Remove metadata items from package.xml before the metadata deployment. |
manual |
Represent a manual step (no CLI execution). |
Run command
Runs a custom command line. In case of multiple commands, use && to separate them.
| Custom parameter | Description | Example |
|---|---|---|
command |
Command line to run (string). | echo "My custom command" |
Example:
- id: removeKnowledgeFlag
label: Remove KnowledgeUser flag
type: command
command: >-
sf data update record --sobject User --where "UserPermissionsKnowledgeUser='true'" --values "UserPermissionsKnowledgeUser='false'" --json
context: all
Import SFDMU project
Runs a SFDMU import for the specified project name. Typically used post-deploy to load records such as templates or reference data.
| Custom parameter | Description | Example |
|---|---|---|
parameters.sfdmuProject |
Name of the SFDMU project to run. | EmailTemplate |
Example:
- id: importTemplates
label: Import email templates
type: data
parameters:
sfdmuProject: EmailTemplate
context: process-deployment-only
Run Apex script
Executes an Apex script file against the target org using sf apex run --file. Useful for initialization scripts or migrations that must run before or after metadata deployment.
| Custom parameter | Description | Example |
|---|---|---|
parameters.apexScript |
Relative path to the .apex script file in the repository. |
scripts/apex/init.apex |
Example:
- id: runInitApex
label: Run initialization apex
type: apex
parameters:
apexScript: scripts/apex/init.apex
context: process-deployment-only
Publish Experience site
Publishes the specified Experience Cloud (community) site using sf community publish. Use this when deployment changes require a publish step.
| Custom parameter | Description | Example |
|---|---|---|
parameters.communityName |
Name of the community/Experience site to publish. | MyExperienceSite |
Example:
- id: publishSite
label: Publish Experience site
type: publish-community
parameters:
communityName: "My Experience Site"
context: process-deployment-only
Schedule batch
Schedules an Apex batch class using System.schedule(). The action verifies that the specified Apex class exists in the org, implements the Schedulable interface, and has a public no-arg constructor. If the class does not meet these requirements, the action fails with a recommendation to use an apex action instead.
If a scheduled job with the same name and cron expression already exists, the action is skipped (idempotent). If a job with the same name but a different cron expression exists, the action fails so you can resolve the conflict manually.
| Custom parameter | Required? | Description | Example |
|---|---|---|---|
parameters.className |
Yes | Name of the Apex class that implements Schedulable with a public no-arg constructor. |
MyBatchScheduler |
parameters.cronExpression |
Yes | Cron expression for the schedule (Salesforce format). | 0 0 0 * * ? |
parameters.jobName |
No | Name of the scheduled job. Defaults to <className>_Schedule if omitted. |
MyBatch_Nightly |
Example:
- id: scheduleNightlyBatch
label: Schedule nightly batch
type: schedule-batch
parameters:
className: MyBatchScheduler
cronExpression: "0 0 0 * * ?"
jobName: MyBatch_Nightly
context: process-deployment-only
Note: If your Schedulable class requires constructor arguments or has a non-public constructor, use an
apexaction with a custom.apexscript instead.
Remove package.xml items
Removes metadata items from the package.xml calculated by hardis:deploy:smart, so they are ignored during the metadata deployment step. Useful to exclude components that are present in git but must not be deployed to the target org (for example org-specific classes or layouts).
Only available as a pre-deploy action. The removal applies to the temporary copies of package.xml used by the deployment, never to the manifest files committed in the repository. It is also compatible with delta deployments: items are removed from the calculated delta package.xml.
runOnlyOnceByOrg is ignored for this action type: since it only alters the current deployment, it runs at every deployment (check and process).
| Custom parameter | Description | Example |
|---|---|---|
parameters.packageXmlItems |
List of items to remove, each in format TypeName:Member1,Member2. Use * as member to remove a whole type. Member names also support glob wildcards (ex: Account*). A single string is also accepted for a single entry. |
ApexClass:MyClass1,MyClass3 |
Example:
- id: removeLegacyItems
label: Remove legacy items from deployment package.xml
type: remove-packagexml-items
parameters:
packageXmlItems:
- ApexClass:MyClass1,MyClass3
- Layout:MyLayout1,MyLayout2,MyLayout3
context: all
Manual step
Marks a manual step that cannot be automated. The Pull Request comments show the instructions (rendered as markdown) and an unchecked box. Once the operator has performed the action, they tick the box: the next job records the action as done for the org branch and skips it from then on (see Confirming manual actions with checkboxes).
| Custom parameter | Description | Example |
|---|---|---|
parameters.instructions |
Human-readable instructions or checklist for the operator/reviewer, in markdown format. Use a YAML block to preserve formatting. |
Example:
- id: url-check
label: Check external callback URL
type: manual
parameters:
instructions: |
Verify that the callback URL in `Setup > Named Credentials` is reachable from the target org and matches the production URL.
context: process-deployment-only


