Installing the Windows MDR Agent Using Atera

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
    
}

main


Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article