This guide provides the steps to installing the Field Effect MDR agent on Windows using Atera. The script requires two inputs to be passed to your RMM:
1. Field Effect MDR Portal API token
The API token must be created in the MDR portal following the instructions in our Help Center: Create an API Key. The Portal account associated with the token must have admin permissions for the script to work properly.
2. Your Atera API key
You can find your Atera API key by signing into Atera with admin credentials. Go to Admin>Data Management>API and click on the 'eye' icon to reveal your key.
To install the agent using Atera
1) Add the Atera Windows MDR Install PowerShell script :
- Go to Admin> Scripts, and click Create Script.
- Ensure the file type is ".ps1"; copy and paste the PowerShell script text from the bottom of this guide into the Atera script editor.
2) Create arguments for the Field Effect API token and Atera API Key:
The Arguments for the script must be passed on a single line in the proper order: Field Effect API token, followed by Atera API key, followed by an empty string.
For example:
"Aaaaz9Y...6l5K4" "a1B2c...3N4o5P6 " ""
3) Save the script and run it against a device or create an Automation profile to run automate the install.
Powershell script for Atera Windows install
# Optional command line params
param (
[string]$api_token,
[string]$ATERA_API_KEY,
[string]$client_name,
[switch]$uninstall,
[switch]$debug
)
# Determine if we are using Atera API or falling back to client_name param
$useAteraAPI = -not [string]::IsNullOrEmpty($ATERA_API_KEY)
if ($useAteraAPI) {
try {
Import-Module PSAtera -MinimumVersion 1.3.1 -ErrorAction Stop
} catch {
Write-Host "PSAtera module not found, attempting to install..."
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -Force -ErrorAction Stop
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
Install-Module PSAtera -MinimumVersion 1.3.1 -Force -AllowClobber -Scope AllUsers -ErrorAction Stop
Import-Module PSAtera -MinimumVersion 1.3.1 -ErrorAction Stop
Write-Host "PSAtera module installed and loaded successfully."
} catch {
Write-Error "Failed to install or load PSAtera module: $_"
exit 1
}
}
try {
Set-AteraAPIKey -APIKey $ATERA_API_KEY -ErrorAction Stop
$agent = Get-AteraAgent
$client_name = (Get-AteraCustomValue -ObjectType Customer -ObjectId $agent.CustomerId).Name
Write-Host "Client name resolved via Atera API: $client_name"
} catch {
Write-Error "Failed to retrieve client name from Atera API: $_"
exit 1
}
} else {
# Fallback: client_name must be provided as a parameter
if ([string]::IsNullOrEmpty($client_name)) {
Write-Error "No ATERA_API_KEY provided and no client_name specified. Please provide one or the other."
exit 1
}
Write-Host "No Atera API key provided, using supplied client name: $client_name"
$client=$client_name
}
# Handle $uninstall as RMM env var
$uninstall = ($env:uninstall -eq "true") -or ($uninstall -eq $true)
$version="v7.2.1"
$win_arch = 64
$mdr_agent_installed = $false
$agent_installer_filename = "x" #look it up
$use_license = $false
$client_valid = $false
$script:StatusFilePath = "C:\ProgramData\Field Effect\Covalence\data\status.json"
# Ensure TLS 1.2 is enabled
[Net.ServicePointManager]::SecurityProtocol = 3072
# Create the "FieldEffect_MDR" log directory if it doesn't exist
$script:installer_filename="x"
$script:installer_path = Join-Path $Env:TMP "FieldEffect_MDR"
$working_folder = $script:installer_path
if (-not (Test-Path $working_folder))
{
New-Item -Path $script:installer_path -Name "FieldEffect_MDR" -ItemType "Directory" | Out-Null
}
# set log file to save in the installing users folder
$debug_log = Join-Path $working_folder "mdrAgentPoShInstaller.proc_$env:computername_$(Get-Date -Format yyyyMMddTHHmmZ).log"
# API token validation
if (-not $api_token -or $api_token.Trim() -eq "") {
$errors += "API token is required. Use -api_token parameter"
}
# Display errors if any
if ($errors.Count -gt 0) {
Write-Host "PARAMETER VALIDATION ERRORS:" -ForegroundColor Red
foreach ($err in $errors) {
Write-Host " - $err" -ForegroundColor Red
}
Write-Host ""
Show-Help
exit 1
}
# WriteLog helper function writes log messaging to log file; 'debug' parameter also outputs logs to screen
function WriteLog
{
Param ([string]$_log_msg)
if ($debug) {
Write-Host $_log_msg
}
$timestamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
$log_msg = "$timestamp $_log_msg"
Add-content $debug_log -value $log_msg
}
#check API token length
if ($api_token.Length -ne 128) {
$log_err = "Portal token invalid; token length is $($api_token.Length)"
WriteLog $log_err
throw $log_err
}
#set API header
$headers = @{
Authorization="Bearer $api_token"
ContentType="application/json"
}
# Set values for startup parameters if provided
$api_token = if (![string]::IsNullOrEmpty($api_token)) { $api_token } else { $null }
$client = if (![string]::IsNullOrEmpty($client)) { $client } else { $null }
$license_key = if (![string]::IsNullOrEmpty($license_key)) { $license_key } else { $null }
# Set architecture type based on processor
$win_arch = if ($env:PROCESSOR_ARCHITECTURE -eq "X86") { 32 } else { 64 }
##********************************************************************************************************
##
## FUNCTION CheckInstall
## Function to check Field Effect MDR agent installation and status
##
## PARAMETERS:
## -WaitForActivation: If specified, waits for agent to reach ACTIVE status
## -TimeoutMinutes: Maximum time to wait for activation (default: 10 minutes)
##
## RETURNS:
## Object with installation details and status
##
##********************************************************************************************************
function CheckInstall {
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[switch]$WaitForActivation,
[Parameter(Mandatory = $false)]
[int]$TimeoutMinutes = 10
)
# Initialize result object
$result = [PSCustomObject]@{
IsInstalled = $false
AgentVersion = $null
CurrentStatus = $null
StatusFileExists = $null
OrganizationId = $null
AgentId = $null
IsActive = $false
ActivationSuccessful = $false
ErrorMessage = $null
TimeoutOccurred = $false
}
try {
WriteLog "Checking agent installation status at: $script:StatusFilePath"
# Check if status file exists
$result.StatusFileExists = Test-Path $script:StatusFilePath
if (-not $result.StatusFileExists) {
WriteLog "Status file not found - agent not installed or installation incomplete"
$result.ErrorMessage = "Status file not found at expected location"
return $result
}
# Read and parse status file
try {
$statusContent = Get-Content -Path $script:StatusFilePath -Raw -ErrorAction Stop
$statusJson = $statusContent | ConvertFrom-Json -ErrorAction Stop
# Validate JSON structure
if (-not $statusJson.AgentInfo) {
throw "Invalid status file structure - AgentInfo section missing"
}
$result.IsInstalled = $true
$result.AgentVersion = $statusJson.AgentInfo.AgentVersion
$result.CurrentStatus = $statusJson.AgentInfo.StateName
$result.OrganizationId = $statusJson.AgentInfo.CurrentOrgId
$result.AgentId = $statusJson.AgentInfo.AgentId
$result.IsActive = ($result.CurrentStatus -eq "ACTIVE")
WriteLog "Agent found - Version: $($result.AgentVersion), Status: $($result.CurrentStatus)"
if ($result.OrganizationId) {
WriteLog "Organization ID: $($result.OrganizationId)"
}
}
catch {
$result.ErrorMessage = "Failed to read or parse status file: $_"
WriteLog $result.ErrorMessage
return $result
}
# Wait for activation if requested
if ($WaitForActivation) {
$result.ActivationSuccessful = Wait-ForAgentActivation -TimeoutMinutes $TimeoutMinutes
$result.TimeoutOccurred = -not $result.ActivationSuccessful -and $result.IsInstalled
# Update final status after waiting
try {
$finalStatus = Get-Content -Path $script:StatusFilePath -Raw | ConvertFrom-Json
$result.CurrentStatus = $finalStatus.AgentInfo.StateName
$result.IsActive = ($result.CurrentStatus -eq "ACTIVE")
}
catch {
WriteLog "Warning: Could not read final status after activation wait"
}
}
return $result
}
catch {
$result.ErrorMessage = "Unexpected error during installation check: $_"
WriteLog $result.ErrorMessage
return $result
}
}
##********************************************************************************************************
##
## HELPER FUNCTION Wait-ForAgentActivation
## Waits for the agent to reach ACTIVE status
##
##********************************************************************************************************
function Wait-ForAgentActivation {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[int]$TimeoutMinutes
)
WriteLog "Waiting for agent activation (timeout: $TimeoutMinutes minutes)"
$timeoutSeconds = $TimeoutMinutes * 60
$pollIntervalSeconds = 10
$maxAttempts = [Math]::Ceiling($timeoutSeconds / $pollIntervalSeconds)
$attempt = 1
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
while ($attempt -le $maxAttempts) {
try {
$statusJson = Get-Content -Path $script:StatusFilePath -Raw | ConvertFrom-Json
$currentStatus = $statusJson.AgentInfo.StateName
$elapsedMinutes = [Math]::Round($stopwatch.Elapsed.TotalMinutes, 1)
WriteLog "Activation check $attempt/$maxAttempts - Status: $currentStatus (Elapsed: ${elapsedMinutes}m)"
if ($currentStatus -eq "ACTIVE") {
$stopwatch.Stop()
WriteLog "Agent successfully activated after ${elapsedMinutes} minutes"
return $true
}
# Check for error states that won't resolve
$errorStates = @("ERROR", "FAILED", "DISABLED")
if ($currentStatus -in $errorStates) {
WriteLog "Agent is in error state: $currentStatus - activation unlikely to succeed"
break
}
if ($attempt -lt $maxAttempts) {
Start-Sleep -Seconds $pollIntervalSeconds
}
$attempt++
}
catch {
WriteLog "Error reading status during activation wait (attempt $attempt): $_"
if ($attempt -lt $maxAttempts) {
Start-Sleep -Seconds $pollIntervalSeconds
}
$attempt++
}
}
$stopwatch.Stop()
$finalElapsed = [Math]::Round($stopwatch.Elapsed.TotalMinutes, 1)
WriteLog "Agent activation timeout after ${finalElapsed} minutes - final status may not be ACTIVE"
return $false
}
##********************************************************************************************************
##
## FUNCTION PreScript
## Performs cursory checks on input parameters. If a client name is provided, also validates client
## name against known clients from portal API.
## Returns nothing.
##
##********************************************************************************************************
function PreScript
{
$org_id="x"
WriteLog "Verifying setup..."
Writelog "Detecting Powershell verison $($PSVersionTable.PSVersion.ToString())"
#check if running as Admin
$check_user_priv = [Security.Principal.WindowsIdentity]::GetCurrent()
$run_as_admin = ([Security.Principal.WindowsPrincipal]($check_user_priv)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $run_as_admin) {
WriteLog "WARNING- user not detected as having admin permission: $($check_user_priv.Name)"
}
#check that client name OR license key is provided
if (($license_key.length -eq 0) -AND ($client.length -eq 0)){
$log_err = "At least one of org license key or client name is required and was not provided"
WriteLog $log_err
throw $log_err
}
#validate license input
if (($license_key.length -ne 0) -AND ($license_key.length -ne 156)){
$log_err = "COVALENCE_LICENSE key was not valid. Key length appears to be $($license_key.length)/expected 156; double-check that all characters were copied"
WriteLog $log_err
throw $log_err
}
if ($license_key.length -eq 156){
$use_license=$true
WriteLog "Detected proper license key: $($license_key.Length)"
return $org_id, $use_license, $false
}
#validate client name input
if(($license_key.length -eq 0) -and ($client.Length -ne 0)){
try {
$client_list = Invoke-RestMethod -Method Get -Uri "https://services.fieldeffect.net/v1/organizations" -Headers $headers
} catch {
$log_err = "Failed to reach Field Effect API. Exception: $($_.Exception.Message). Inner: $($_.Exception.InnerException.Message)"
WriteLog $log_err
throw $log_err
}
$client_list = $client_list -creplace '"id"','"new_ID"' | ConvertFrom-JSON
# Check if the client matches name or client_id
$client_valid = $false
foreach ($c in $client_list) {
if ($client -like $c.name -or $client -like $c.client_id) {
$client_valid = $true
$org_id = $c.ID
WriteLog "Proceeding to find license key for $($c.name)."
return $org_id, $false, $client_valid
break # Exit loop early once a match is found
}
}
# Handle invalid client
if (-not $client_valid){
$log_err = "Client name/id $($c.name)is not valid. It must be one of:"
WriteLog $log_err
$client_list | ForEach-Object { WriteLog "$($_.name) ($($_.client_id))" }
throw "Client name/id $($c.name) is not valid"
}
}#endif
} #end PreScript
function GetOrgid
{
param (
[Parameter(Mandatory = $true)][boolean] $_use_license,
[Parameter(Mandatory = $true)][boolean] $_client_valid,
[Parameter(Mandatory = $true)][string] $_org_id
)
$org_id="x"
##get ORGID:
#if license key provided
#get client organization(s) and use ID of first returned org; the license key accompanying the bundle zip will be ignored
#if license key not provided
#Use client name provided to get client ID from name; the license key from the bundle zip will be used
if($_use_license){
#get v1/organizations and use [0] to get orgid of user and download generic bundle using passed in license key
$generic_client= Invoke-RestMethod -Method Get -Uri "https://services.fieldeffect.net/v1/organizations" -Headers $headers
$generic_client = $generic_client -creplace '"id"','"new_ID"' | ConvertFrom-JSON
$org_id=$generic_client[0].ID
}
elseif ($_client_valid){
#use orgid value already set under pre-script
$org_id = $_org_id
}
else {
throw "Client name/id or license key not provided"
}
return $org_id
}
##********************************************************************************************************
##
## FUNCTION DownloadInstaller
## Passed the org_id, downloads the installer bundle from the portal API, unzips the msi and
## license files to the temp folder, then deletes the downloaded archive.
## Returns nothing.
##
##********************************************************************************************************
function DownloadInstaller
{
param (
[Parameter(Mandatory = $true)][string] $_org_id
)
WriteLog "Downloading install bundle to $script:installer_path..."
$installer_body = @{
bundle_constituents = @(
@{
architecture = "$win_arch bit"
platform = "Windows"
}
)
organization_id = $_org_id
} | ConvertTo-Json
$bundle_response = Invoke-RestMethod -Method Post -Uri "https://services.fieldeffect.net/v1/endpoint_installer_bundles" -Body $installer_body -Headers $headers -ContentType application/json
# Define paths
# $zipFileName = $bundle_response.file_name
$sanitizedName = 'Field_Effect_MDR_agent_installer.zip'
$zipFilePath = Join-Path $working_folder $sanitizedName
$extractFolderName = [System.IO.Path]::GetFileNameWithoutExtension($sanitizedName)
$extractPath = Join-Path $working_folder $extractFolderName
# Remove any leftover files or folders from previous runs
if (Test-Path $zipFilePath) {
Remove-Item $zipFilePath -Force
}
if (Test-Path $extractPath) {
Remove-Item $extractPath -Recurse -Force
}
# Download the ZIP file
try {
$msi = New-Object System.Net.WebClient
$msi.DownloadFile($bundle_response.download_link, $zipFilePath)
$msi.Dispose()
WriteLog "Downloaded ZIP to $zipFilePath"
} catch {
WriteLog "Download failed: $_"
throw "Download error: $_"
}
# Validate file before extraction
if (-not (Test-Path $zipFilePath)) {
throw "ZIP file was not found at $zipFilePath"
}
try {
Expand-Archive -Path $zipFilePath -DestinationPath $extractPath -Force
WriteLog "Extracted ZIP contents to $extractPath"
} catch {
WriteLog "Extraction failed: $_"
throw "Failed to extract ZIP file: $_"
}
# Set final directory of msi installer file
$script:installer_path = $extractPath
$script:installer_filename = Get-ChildItem -Path $extractPath -Filter *.msi | Select-Object -First 1
if (-not $script:installer_filename) {
throw "MSI installer not found in extracted contents"
}
WriteLog "Installer ready at $script:installer_path\$script:installer_filename"
}
##********************************************************************************************************
##
## FUNCTION GetLicense
## Passed a use_license bool, pulls the license_key from the input parameter on TRUE or from the
## downloaded installer bundle on FALSE.
## Returns the license key.
##
##********************************************************************************************************
function GetLicense
{
param (
[Parameter(Mandatory = $true)][boolean] $_use_license
)
$mask_length=8
if($_use_license) {
#use license_key as typed
WriteLog "Using provided license key: "
$license_key = $license_key
# clean up unused license.txt file
Remove-Item -Path (Join-Path $script:installer_path "license.txt")
}
else {
$license_key = Get-Content -Path (Join-Path $script:installer_path "license.txt")
# clean up license.txt file
WriteLog "Using license key from downloaded bundle: "
Remove-Item -Path (Join-Path $script:installer_path "license.txt")
}
$masked_key = $license_key.Substring(0, $mask_length) + '*' * ($license_key.Length - $mask_length - $mask_length) + $license_key.Substring($license_key.Length - $mask_length)
WriteLog $masked_key
return $license_key
}
##********************************************************************************************************
##
## FUNCTION InstallAgent
## Passed the license key, performs a silent install using msiexec. Msiexec logs are saved in the
## working folder.
## Returns nothing
##
##********************************************************************************************************
function InstallAgent
{
param (
[Parameter(Mandatory = $true)][string] $_license_key
)
WriteLog "Installing agent"
# Construct the msiexec argument list
$msiPath = Join-Path $script:installer_path $script:installer_filename
$logPath = Join-Path $script:installer_path "msiexec_log.txt"
$arguments = "/i `"$msiPath`" COVALENCE_LICENSE=`"$_license_key`" /qn /l*v `"$logPath`""
# Run the installation process and wait for completion
Start-Process msiexec.exe -Wait -ArgumentList $arguments
WriteLog "msiexec logs written to:"
WriteLog " $logpath"
Start-Sleep -Seconds 1
if (-not (Test-Path "C:\ProgramData\Field Effect\Covalence\data\status.json")){
WriteLog "Checking installation status"
Start-Sleep -Seconds 5
if (-not (Test-Path "C:\ProgramData\Field Effect\Covalence\data\status.json")){
$log_err="Installation did NOT succeed. No status file found."
WriteLog $log_err
throw $log_err
}
}
}
#######################################################################################################################
## MAIN
##
#######################################################################################################################
function main
{
#run pre-flight checks
$org_id, $use_license, $client_valid = Prescript
#check for exsiting agent
$installCheck = CheckInstall
if ($installCheck.IsInstalled) {
Writelog "Agent is installed - Version: $($installCheck.AgentVersion), Status: $($installCheck.CurrentStatus)"
exit 0
} else {
Writelog "Agent is not installed: $($installCheck.ErrorMessage)"
}
$org_id = GetOrgid $use_license $client_valid $org_id
DownloadInstaller $org_id
$license_key = GetLicense $use_license
InstallAgent $license_key
}
mainWas this article helpful?
That’s Great!
Thank you for your feedback
Sorry! We couldn't be helpful
Thank you for your feedback
Feedback sent
We appreciate your effort and will try to fix the article