Streamlining Custom Script Development in D365 Finance & Operations with GIT
- Parker Jacobs
- 24 minutes ago
- 4 min read

Custom scripts are a powerful way to perform data corrections in Finance & Operations (F&O) environments. At PS Hummingbird, we’ve refined a process for developing, managing, and deploying custom scripts using GIT and Azure DevOps that keeps your codebase organized and your deployments smooth.
Why Use GIT for Custom Script Development?
Organizing custom scripts in GIT offers several advantages:
Branch Organization: Scripts are managed in dedicated branches, nested in a “script” folder.
Permanent Repository Home: Scripts remain accessible without cluttering the main branch.
Simplified Builds: No need for cloud-hosted environments or local VMs.
Improved Audit Trail: Every deployable package and change is tracked.
Getting Started
Configure your GIT repository for custom script development.
Create a new branch based on main (production branch) and name the branch, Scripts (or a name that aligns with existing branch naming conventions such as 04-Scripts).
Create a new Visual Studio solution named Build-CustomScripts and add the 'Build - Custom Scripts.yml' file to your Scripts branch. For now, the project's model can be left to the default model of Fleet Management.
Build - Custom Scripts.yml Source Code
# Change the name of the build to a 4-digit version number to be used for model versioning
name: $(Date:yy.MM.dd)$(Rev:.r)
trigger:
- script/<branch name here>
pool:
# Use the VS2019 image
vmImage: 'windows-latest'
demands:
- msbuild
- visualstudio
# Declare some shorthand for NuGet package names
# Make editing the path for metadata and NuGet extraction folder easier
variables:
App1Package: 'Microsoft.Dynamics.AX.Application1.DevALM.BuildXpp'
App2Package: 'Microsoft.Dynamics.AX.Application2.DevALM.BuildXpp'
AppSuitePackage: 'Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp'
PlatPackage: 'Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp'
ToolsPackage: 'Microsoft.Dynamics.AX.Platform.CompilerPackage'
MetadataPath: '$(Build.SourcesDirectory)\Metadata'
NugetConfigsPath: '$(Build.SourcesDirectory)\Metadata'
NugetsPath: '$(Pipeline.Workspace)\NuGets'
steps:
# Install NuGet and use -ExcludeVersion option to avoid paths containing version numbers
- task: NuGetCommand@2
displayName: 'NuGet custom install Packages'
inputs:
command: custom
arguments: 'install -Noninteractive $(NugetConfigsPath)\packages.config -ConfigFile $(NugetConfigsPath)\nuget.config -Verbosity Detailed -ExcludeVersion -OutputDirectory "$(NugetsPath)"'
# Use the custom build number y.m.d.rev as the model version
# Default updates only VAR layer and above
- task: XppUpdateModelVersion@0
displayName: 'Update Model Version'
inputs:
XppSourcePath: '$(MetadataPath)'
VersionNumber: '$(Build.BuildNumber)'
# Build using MSBuild 16 (VS 2019)
# Provide the needed paths, including semi-colon separated list of reference folders
# /p:ReferenceFolder are metadata folders containing other (compiled) X++ packages that are referenced
# /p:ReferencePath are folders containing non-X++ assemblies referenced (aside from one already in the output folder for the package)
- task: VSBuild@1
displayName: 'Build solution **\*.sln'
inputs:
solution: '$(Build.SourcesDirectory)/Projects/Build-CustomScripts/*.sln'
vsVersion: '17.0'
msbuildArgs: '/p:BuildTasksDirectory="$(NugetsPath)\$(ToolsPackage)\DevAlm" /p:MetadataDirectory="$(MetadataPath)" /p:FrameworkDirectory="$(NuGetsPath)\$(ToolsPackage)" /p:ReferenceFolder="$(NuGetsPath)\$(PlatPackage)\ref\net40;$(NuGetsPath)\$(App1Package)\ref\net40;$(NuGetsPath)\$(App2Package)\ref\net40;$(NuGetsPath)\$(AppSuitePackage)\ref\net40;$(MetadataPath);$(Build.BinariesDirectory)" /p:ReferencePath="$(NuGetsPath)\$(ToolsPackage)" /p:OutputDirectory="$(Build.BinariesDirectory)"'
# Remove System.Core.dll for custom script deployable package. Causes errors when uploading to D365.
- task: DeleteFiles@1
displayName: 'Delete File System.Core.dll'
inputs:
SourceFolder: '$(Build.BinariesDirectory)'
Contents: |
**/bin/System.Core.dll
# Copy the compiler log files to the drop artifacts
- task: CopyFiles@2
displayName: 'Copy X++ Compile Log Files to: $(Build.ArtifactStagingDirectory)\Logs'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**\Dynamics.AX.*.xppc.*
**\Dynamics.AX.*.labelc.*
**\Dynamics.AX.*.reportsc.*
TargetFolder: '$(build.ArtifactStagingDirectory)\Logs'
condition: succeededOrFailed()
# For packaging we need NuGet installed, with a version <3.4.0
- task: NuGetToolInstaller@0
displayName: 'Use NuGet 3.3.0'
inputs:
versionSpec: 3.3.0
- task: XppCreatePackage@2
displayName: 'Create Deployable Package'
inputs:
XppToolsPath: '$(NuGetsPath)\$(ToolsPackage)'
CreateCloudPackage: false
CreateRegularPackage: true
# Enable this task to add a license file to the package
- task: XppAddLicenseToPackage@0
displayName: 'Add Licenses to Deployable Package'
enabled: false
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)'
condition: succeededOrFailed()Commit your changes to the Scripts branch to establish the foundation for future scripts.
Screenshots of the repository layout
Top-level:

Project Folder Breakdown:

The Custom Script Process Flow
Branch Creation
Create a new branch from Scripts, associated with a work item in Azure DevOps.
Prefix the branch with 'script/' and use a descriptive format:
<Company ID>_<Work item #>_<Script description>_<Developer Initials>
Example: PSHB_5_DeleteSO000860_JPJ

Development Steps
Checkout the branch in Visual Studio.
Create a new model, solution, and project matching the branch name.
Repo Structure:

Develop your class following Microsoft standards for custom scripts.
Example Script:

Build and Update Artifacts
Update the Build-CustomScripts solution to reference your custom model.

Commit and push your changes.
Pipeline Setup
In Azure DevOps, create a script folder and a new pipeline based on your Build - Custom Scripts.yml file.
Name the pipeline informatively, e.g., Script Pipeline - <model name>.

Execute the pipeline to create your deployable package.
Deployment
Follow Microsoft’s instructions for uploading and executing the script in your D365FO environment.
Merge to Scripts Branch
Create a Pull Request and merge the working branch, script/PSHB_5_DeleteSO000860_JPJ in this example, into the Scripts branch.
Delete the working branch after completing the work.
Special Notes
Branch Management: Custom script branches are not merged back into main. This keeps your main branch clean and focused.
Work Item Resolution: Move work items to “resolved” after script completion.
YML File Insights
The Build - Custom Scripts.yml file contains a unique step that removes a file that is included during the Visual Studio Build task of the pipeline. When the build is executing, the file, System.Core.dll, is copied into the build pipeline's deployable package binary output. The following screenshot shows a snippet from the build pipeline log of where the file is included:
System.Core.dll is copied as a reference during the Visual Studio Build task's execution

If System.Core.dll is not removed, an error will be thrown when attempting to upload the deployable package to a D365FO environment. The error is triggered due to the existence of more than one DLL file (custom model's DLL and System.Core.dll) within the deployable package being uploaded. To resolve this issue, the delete task can be added after the Visual Studio Build task to remove System.Core.dll from the output:
Delete Task

The delete task removes the DLL file from the bin folder allowing for a successful upload of the deployable package to the target environment.
Benefits Recap
Organized Repository: Scripts are easy to find and manage.
No Merge Clutter: Main branch stays clean.
Efficient Builds: No extra infrastructure needed.
Clear Audit Trail: Every change is tracked.
Resources
Ready to streamline your custom script development? Try this approach and let us know your feedback!
