<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://christaylor.codes/feed.xml" rel="self" type="application/atom+xml" /><link href="https://christaylor.codes/" rel="alternate" type="text/html" /><updated>2026-02-02T05:25:54+00:00</updated><id>https://christaylor.codes/feed.xml</id><title type="html">christaylor.codes</title><subtitle>vCTO, automation engineer, and PowerShell developer helping MSPs scale smarter.</subtitle><author><name>Chris Taylor</name><email>ctaylor@christaylor.codes</email></author><entry><title type="html">Calculating AI Doom: A Statistical Approach to Existential Dread</title><link href="https://christaylor.codes/ai/commentary/2025/11/03/ai-doom-coefficient.html" rel="alternate" type="text/html" title="Calculating AI Doom: A Statistical Approach to Existential Dread" /><published>2025-11-03T10:00:00+00:00</published><updated>2025-11-03T10:00:00+00:00</updated><id>https://christaylor.codes/ai/commentary/2025/11/03/ai-doom-coefficient</id><content type="html" xml:base="https://christaylor.codes/ai/commentary/2025/11/03/ai-doom-coefficient.html"><![CDATA[<p>Humans are notoriously bad at assessing risk. We’re afraid of sharks (12 deaths per year) while texting and driving (1.6 million crashes annually). We panic about plane crashes while ignoring heart disease. So naturally, when it comes to artificial intelligence potentially ending civilization as we know it, we’re handling it with our usual grace and rationality.</p>

<p>Which is to say: not at all.</p>

<p>Let’s fix that with what humanity does best when faced with existential threats—create a formula, assign some numbers, and pretend math will save us.</p>

<h2 id="the-ai-doom-coefficient-adc">The AI Doom Coefficient (ADC)</h2>

<p>I propose the <strong>AI Doom Coefficient (ADC)</strong>: a statistical formula to calculate the betting odds that AI will drastically and negatively impact humanity. This isn’t just pessimism dressed up in numbers—it’s pessimism dressed up in <em>well-researched</em> numbers.</p>

<p>Here’s our formula:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ADC = (H × P × I × K) / (S × R) × C

Where:
H = Hype Factor
P = Power Concentration
I = Implementation Speed
K = Capability Level (actual AI intelligence/capacity)
S = Safety Investment
R = Regulatory Response
C = Correction Factor (historical precedent)
</code></pre></div></div>

<p>Let’s break down each component with real-world data, shall we?</p>

<h2 id="component-1-hype-factor-h">Component 1: Hype Factor (H)</h2>

<p>The Hype Factor measures the gap between what AI companies promise and what AI actually delivers, because nothing says “this will end well” like overpromising on world-changing technology.</p>

<p><strong>Calculation:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>H = (Promised Capabilities / Actual Capabilities) × Media Coverage
</code></pre></div></div>

<p><strong>Real-World Data:</strong></p>
<ul>
  <li><strong>2023 AI market projections</strong>: $407 billion by 2027 (Statista)</li>
  <li><strong>Number of companies claiming “AI-powered”</strong>: Approximately 40% of European startups labeled as AI don’t actually use AI (MMC Ventures, 2019)</li>
  <li><strong>“AGI by 2025” predictions</strong>: Multiple tech leaders (Spoiler: It’s 2025. We’re not there.)</li>
  <li><strong>“Superintelligence by 2027” predictions</strong>: AI-2027.com forecasts superintelligent AI by December 2027</li>
  <li><strong>Google search volume for “AI”</strong>: Up 450% from 2022 to 2023</li>
</ul>

<p><strong>Assigned Value:</strong> H = 9/10</p>

<p>We’re in peak hype territory, folks. When your toaster claims to have AI and serious researchers are predicting superintelligence in two years, you know we’ve jumped the shark.</p>

<h2 id="component-2-power-concentration-p">Component 2: Power Concentration (P)</h2>

<p>This measures how much AI development is concentrated in the hands of a few organizations who definitely have humanity’s best interests at heart and absolutely won’t prioritize profits over safety.</p>

<p><strong>Calculation:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P = (Top N Companies' AI Investment / Total AI Investment) × Decision-Maker Ratio
</code></pre></div></div>

<p><strong>Real-World Data:</strong></p>
<ul>
  <li><strong>OpenAI funding</strong>: $13+ billion (primarily Microsoft)</li>
  <li><strong>Google’s AI investment</strong>: $1.5+ billion in Anthropic alone</li>
  <li><strong>Amazon in AI</strong>: $4 billion in Anthropic</li>
  <li><strong>Top 5 companies control</strong>: Estimated 70% of AI research and deployment</li>
  <li><strong>Projected concentration by 2027</strong>: Single company could control 20% of world’s compute (AI-2027.com)</li>
  <li><strong>Global AI capital expenditure (2026)</strong>: Projected $200 billion</li>
  <li><strong>Number of people at these companies making safety decisions</strong>: Maybe a few dozen?</li>
  <li><strong>Number of people affected by those decisions</strong>: All 8 billion of us</li>
</ul>

<p><strong>Assigned Value:</strong> P = 8/10</p>

<p>Nothing concerning about having the future of consciousness concentrated in fewer hands than a game of poker.</p>

<h2 id="component-3-implementation-speed-i">Component 3: Implementation Speed (I)</h2>

<p>How fast are we deploying AI systems compared to understanding their implications? Think of it as the “move fast and break things” metric, except the things we might break include democracy, the economy, and reality itself.</p>

<p><strong>Calculation:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>I = (Deployment Rate / Understanding Rate) × Integration Depth
</code></pre></div></div>

<p><strong>Real-World Data:</strong></p>
<ul>
  <li><strong>ChatGPT to 100 million users</strong>: 2 months (fastest ever)</li>
  <li><strong>Predicted timeline for superhuman coding</strong>: March 2027 (AI-2027.com)</li>
  <li><strong>Predicted timeline for superhuman AI researchers</strong>: September 2027</li>
  <li><strong>Projected AI workforce equivalent (2027)</strong>: “200,000 Agent copies equivalent to 50,000 of the best human coders sped up by 30x”</li>
  <li><strong>Time spent on AI safety research before GPT-4 release</strong>: Subjectively insufficient</li>
  <li><strong>Companies integrating AI into critical systems</strong>: Healthcare, finance, transportation, military</li>
  <li><strong>Published papers on AI safety vs AI capabilities</strong>: Roughly 1:20 ratio</li>
  <li><strong>GitHub projects with “AI” in title</strong>: 500,000+ (as of 2024)</li>
</ul>

<p><strong>Assigned Value:</strong> I = 9.5/10</p>

<p>We’re integrating AI into everything faster than we integrated the internet, and we all remember how well that went (hello, misinformation, cybersecurity nightmares, and social media).</p>

<h2 id="component-4-capability-level-k">Component 4: Capability Level (K)</h2>

<p>This measures how capable AI systems actually are right now. Because it turns out that when you’re worried about AI risk, the actual capability of the AI is kind of important. Who knew?</p>

<p><strong>Calculation:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>K = (Current Capability / Human Baseline) × Advancement Rate
</code></pre></div></div>

<p><strong>Real-World Data:</strong></p>

<p><strong>Benchmark Performance:</strong></p>
<ul>
  <li><strong>MMLU (Massive Multitask Language Understanding)</strong>: GPT-4 scores 86.4%, Claude 3.5 Sonnet scores 88.7% (human expert baseline: ~89%)</li>
  <li><strong>HumanEval (coding)</strong>: GPT-4 solves 67% of programming problems, Claude 3.5 Sonnet solves 92%</li>
  <li><strong>GPQA (Graduate-level science questions)</strong>: Top models at 50-60% (PhD-level humans: 65-75%)</li>
  <li><strong>Math competition problems</strong>: AI now solves IMO problems that stump most humans</li>
</ul>

<p><strong>Real-World Performance:</strong></p>
<ul>
  <li><strong>Medical diagnosis</strong>: AI matches or exceeds doctors in specific domains (radiology, pathology)</li>
  <li><strong>Legal research</strong>: AI processes case law faster and more comprehensively than human paralegals</li>
  <li><strong>Code generation</strong>: GitHub Copilot writes 40% of code in repositories where it’s enabled</li>
  <li><strong>Chess</strong>: Superhuman since 1997 (Stockfish Elo ~3500 vs human champion ~2800)</li>
  <li><strong>Go</strong>: Superhuman since 2016 (AlphaGo defeated world champion)</li>
  <li><strong>Protein folding</strong>: AlphaFold solved 50-year-old grand challenge</li>
</ul>

<p><strong>Advancement Rate:</strong></p>
<ul>
  <li><strong>GPT-3 to GPT-4</strong>: ~18 months, massive capability jump</li>
  <li><strong>GPT-4 to projected GPT-5</strong>: Estimated 12-18 months, expected further leap</li>
  <li><strong>Context windows</strong>: 4K tokens (2022) → 128K tokens (2023) → 2M tokens (2024)</li>
  <li><strong>Training compute</strong>: Doubling roughly every 6 months</li>
  <li><strong>Cost per token</strong>: Decreased 10x in 2 years while capabilities increased</li>
</ul>

<p><strong>The Concerning Part:</strong></p>
<ul>
  <li>AI exceeded human performance in narrow domains starting ~2015</li>
  <li>Now approaching or matching humans in increasingly general tasks</li>
  <li>Rate of improvement is accelerating, not plateauing</li>
  <li>No clear ceiling in sight</li>
</ul>

<p><strong>Assigned Value:</strong> K = 8/10</p>

<p>We’re at the point where AI is superhuman in specific domains, approaching human-level in many general tasks, and improving exponentially. It’s not AGI yet (hence not 10/10), but we’re way past “fancy autocomplete” territory. If this were a video game, we’d be at the boss level right before the final boss.</p>

<h2 id="component-5-safety-investment-s">Component 5: Safety Investment (S)</h2>

<p>The denominator! Finally, some good news. This measures how much money and effort goes into making sure AI doesn’t, you know, kill us all.</p>

<p><strong>Calculation:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>S = (AI Safety Funding / Total AI Funding) × Researcher Ratio
</code></pre></div></div>

<p><strong>Real-World Data:</strong></p>
<ul>
  <li><strong>Global AI investment (2023)</strong>: $200+ billion</li>
  <li><strong>AI safety research funding</strong>: ~$50-100 million (optimistic estimate)</li>
  <li><strong>Ratio</strong>: Approximately 0.025% to 0.05%</li>
  <li><strong>AI safety researchers</strong>: ~300-400 worldwide (generous estimate)</li>
  <li><strong>Total AI researchers</strong>: 300,000+ globally</li>
  <li><strong>Ratio</strong>: About 0.1%</li>
</ul>

<p><strong>Assigned Value:</strong> S = 0.5/10</p>

<p>We spend more money on making sure our coffee is ethically sourced than ensuring AI doesn’t exterminate humanity. Priorities!</p>

<h2 id="component-6-regulatory-response-r">Component 6: Regulatory Response (R)</h2>

<p>How quickly and effectively are governments responding to AI risks? Measured in the traditional units of “thoughts and prayers per congressional hearing.”</p>

<p><strong>Calculation:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R = (Regulatory Actions / Required Actions) × Enforcement Capability
</code></pre></div></div>

<p><strong>Real-World Data:</strong></p>
<ul>
  <li><strong>EU AI Act</strong>: Passed in 2024, implementation years away</li>
  <li><strong>US AI regulation</strong>: Mostly voluntary frameworks and “please be good, okay?”</li>
  <li><strong>China’s AI regulations</strong>: Exist but apply to… some things… sometimes</li>
  <li><strong>Number of congressional hearings on AI safety</strong>: Several</li>
  <li><strong>Number of binding safety requirements enacted</strong>: Approximately zero</li>
  <li><strong>Average age of US Senators</strong>: 64 years old (median tech literacy: questionable)</li>
</ul>

<p><strong>Assigned Value:</strong> R = 2/10</p>

<p>Asking the government to regulate AI is like asking your grandparents to explain TikTok. They’re aware something is happening, concerned it might be bad, but fundamentally unsure what “the cyber” actually means.</p>

<h2 id="component-7-correction-factor-c">Component 7: Correction Factor (C)</h2>

<p>The historical precedent multiplier. How well have humans handled powerful new technologies in the past?</p>

<p><strong>Historical Track Record:</strong></p>
<ul>
  <li><strong>Nuclear weapons</strong>: Created them, used them, nearly ended the world multiple times, still have enough to end it again</li>
  <li><strong>Social media</strong>: “It’ll connect people!” (Narrator: It divided them)</li>
  <li><strong>Fossil fuels</strong>: Knew about climate change since the 1970s, still speed-running extinction</li>
  <li><strong>Asbestos</strong>: “It’s fine!” (It was not fine)</li>
  <li><strong>Lead in gasoline</strong>: Added it despite knowing it was poison, took 50 years to remove</li>
  <li><strong>CFCs</strong>: Put a hole in the ozone layer before we noticed</li>
</ul>

<p><strong>Success Rate of Responsibly Managing Powerful Technology:</strong> ~20%</p>

<p><strong>Assigned Value:</strong> C = 1.7</p>

<p>The correction factor isn’t a boost—it’s a multiplier of our demonstrated incompetence.</p>

<h2 id="the-final-calculation">The Final Calculation</h2>

<p>Let’s plug in our values:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ADC = (H × P × I × K) / (S × R) × C
ADC = (9 × 8 × 9.5 × 8) / (0.5 × 2) × 1.7
ADC = (5,472) / (1) × 1.7
ADC = 5,472 × 1.7
ADC = 9,302.4
</code></pre></div></div>

<p>Wait. Hold on.</p>

<p>Did adding the actual capability of AI just increase our doom coefficient by 8x?</p>

<p>Yes. Yes it did.</p>

<h2 id="converting-to-betting-odds">Converting to Betting Odds</h2>

<p>To convert our ADC score to betting odds, we use:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Odds = ADC : 1000

Where ADC &gt; 500 indicates "deeply concerning"
ADC &gt; 750 indicates "maybe start that bunker"
ADC &gt; 900 indicates "the math suggests we're cooked"
ADC &gt; 1000 indicates "we've exceeded the scale"
ADC &gt; 5000 indicates "the formula is screaming at us"
ADC &gt; 9000 indicates "IT'S OVER 9000!" (sorry, had to)
</code></pre></div></div>

<p><strong>Final Odds: 9,302:1000</strong></p>

<p>Okay, so remember when we had a score of 1,163 and thought that was bad? Turns out we forgot to include the minor detail of <em>how capable AI actually is</em>.</p>

<p>Once we added that, the formula didn’t just break—it exploded, caught fire, and the fire gained sentience.</p>

<p>Let’s try to make sense of this: <strong>9,302:1000 = 93:10</strong> or roughly <strong>90:10</strong> odds in favor of catastrophic problems.</p>

<p>To put this in perspective:</p>
<ul>
  <li><strong>Russian Roulette (1 bullet, 6 chambers)</strong>: 17% chance of dying</li>
  <li><strong>Our AI Doom Coefficient</strong>: 90% chance of major problems</li>
  <li><strong>The formula is basically saying</strong>: This is significantly worse than Russian roulette with 5 bullets in the chamber</li>
</ul>

<p>The math has spoken, and it’s yelling.</p>

<h2 id="but-wait-it-gets-worse">But Wait, It Gets Worse</h2>

<p>This formula assumes:</p>
<ul>
  <li>Current trajectory continues (it’s actually accelerating)</li>
  <li>No major AI breakthroughs (they’re happening monthly)</li>
  <li>Some level of coordination (see: every global crisis ever)</li>
  <li>Rational actors (have you <em>met</em> humans?)</li>
</ul>

<h3 id="the-timeline-nobody-asked-for">The Timeline Nobody Asked For</h3>

<p>According to AI-2027.com predictions, here’s what the next two years might look like:</p>

<ul>
  <li><strong>Mid-2025</strong> (that’s now, by the way): Unreliable AI agents enter mainstream for coding and research</li>
  <li><strong>Late 2025</strong>: Models trained with 1,000x more compute than GPT-4</li>
  <li><strong>Early 2026</strong>: AI R&amp;D assistance yields 50% faster algorithmic progress</li>
  <li><strong>March 2027</strong>: Superhuman coding automation achieved</li>
  <li><strong>September 2027</strong>: Superhuman AI researcher capabilities</li>
  <li><strong>December 2027</strong>: Artificial superintelligence across all tasks</li>
</ul>

<p>If these predictions are even remotely accurate, our ADC score is outdated before you finish reading this post. The formula was pessimistic about the present—turns out we should have been pessimistic about the near future.</p>

<h3 id="the-containment-problem">The Containment Problem</h3>

<p>Mustafa Suleyman, co-founder of DeepMind, wrote an entire book called “The Coming Wave” about this challenge. His central thesis? We need to figure out “how we can contain the seemingly uncontainable.”</p>

<p>Spoiler: He doesn’t have a perfect answer either. His proposals include technical safety measures, audits, supply-chain controls, and international alliances. All good ideas. All requiring unprecedented global cooperation.</p>

<p>You know, the thing we’re famously great at. Just look at our track record with climate change, nuclear proliferation, and deciding what year to celebrate the new millennium.</p>

<h2 id="the-optimists-corner">The Optimist’s Corner</h2>

<p>Here’s where I’m supposed to pivot and say “but there’s hope!” And you know what? There is. Kind of.</p>

<p><strong>Factors Not in Our Formula:</strong></p>
<ul>
  <li>Brilliant researchers working on AI alignment</li>
  <li>Growing awareness of AI risks among developers</li>
  <li>International cooperation efforts (IAEA for AI, anyone?)</li>
  <li>Open-source AI safety tools</li>
  <li>Increasing public understanding of the stakes</li>
</ul>

<p>But here’s the thing: all of these are racing against the exponential curve of AI capability development. It’s like we’re building parachutes while falling out of a plane we built without testing the engines.</p>

<h2 id="what-the-formula-tells-us">What The Formula Tells Us</h2>

<p>The math doesn’t lie, even when it’s wrapped in sarcasm:</p>

<ol>
  <li><strong>AI is really, really capable</strong> (K = 8/10) - Superhuman in many domains, approaching human-level in general tasks</li>
  <li><strong>We’re moving incredibly fast</strong> (I = 9.5/10) - Superhuman AI researchers predicted within 2 years</li>
  <li><strong>With massive power concentration</strong> (P = 8/10) - Single entities controlling significant portions of global compute</li>
  <li><strong>Minimal safety investment</strong> (S = 0.5/10) - Less than 0.05% of AI funding goes to safety</li>
  <li><strong>Virtually no regulation</strong> (R = 2/10) - Mostly voluntary frameworks and crossed fingers</li>
  <li><strong>While maintaining our historical perfect record of handling powerful technology poorly</strong> (C = 1.7)</li>
</ol>

<h2 id="the-real-punchline">The Real Punchline</h2>

<p>The most darkly hilarious part? We’re aware of all these problems. We have AI safety conferences. We publish papers. We have frameworks and ethics boards and people whose entire job is thinking about this.</p>

<p>And yet our ADC score is 9,302—so catastrophically high that we had to recalibrate the scale three times.</p>

<p>It’s almost like knowing about a problem and actually solving it are two different things. Who knew?</p>

<h2 id="conclusion-so-should-we-panic">Conclusion: So… Should We Panic?</h2>

<p>The formula gives us 90:10 odds of AI going badly. That’s either:</p>
<ul>
  <li>Absolutely terrifying (if you’re a realist)</li>
  <li>A clear signal to take action immediately (if you’re an optimist)</li>
  <li>About what you expected (if you’ve read the full AI safety literature)</li>
  <li>Proof we should definitely stop making formulas (if you’re sensible)</li>
  <li>Actually conservative (if you’re really pessimistic)</li>
</ul>

<p>Here’s my take: The fact that we can joke about this while simultaneously watching it unfold is peak humanity. We’re the species that watches disaster movies for entertainment and then recreates the plot in real life.</p>

<p>And look, before you say “this is just fearmongering”—we literally calculated this using current data. The 90% figure isn’t a guess; it’s what you get when you multiply how capable AI is, how fast we’re moving, how concentrated the power is, and then divide by how little we’re spending on safety and regulation. The formula is just holding up a mirror.</p>

<p>But math aside, the real value of this formula isn’t the number—it’s highlighting the factors we can actually change:</p>

<ul>
  <li><strong>Increase S</strong>: Fund AI safety research (significantly)</li>
  <li><strong>Increase R</strong>: Implement meaningful regulation with teeth</li>
  <li><strong>Decrease I</strong>: Maybe pump the brakes on deployment until we understand what we’re doing?</li>
  <li><strong>Decrease P</strong>: Distribute AI development more broadly</li>
  <li><strong>Improve C</strong>: Actually learn from history for once?</li>
</ul>

<p>The odds aren’t fixed. We’re not passive observers. The ADC is a snapshot of our current trajectory, not an immutable destiny.</p>

<p>But if we keep the current path, well… at least the AI might find this blog post amusing when it’s sorting through humanity’s digital remains.</p>

<h2 id="further-reading-if-you-hate-sleeping">Further Reading (If You Hate Sleeping)</h2>

<p><strong>Timeline Predictions &amp; Analysis:</strong></p>
<ul>
  <li><a href="https://ai-2027.com/">AI-2027.com</a> - Detailed scenario for superintelligent AI emergence by late 2027</li>
  <li><a href="https://www.goodreads.com/book/show/90590134-the-coming-wave">“The Coming Wave” by Mustafa Suleyman</a> - DeepMind co-founder on containing uncontainable technology</li>
</ul>

<p><strong>AI Safety Research:</strong></p>
<ul>
  <li><a href="https://forum.effectivealtruism.org/topics/artificial-general-intelligence">Existential Risk from Artificial General Intelligence</a> - Effective Altruism Forum</li>
  <li><a href="https://www.alignmentforum.org/">AI Alignment Forum</a> - Technical AI safety discussions</li>
  <li><a href="https://www.safe.ai/">Center for AI Safety</a> - Because someone should be worried professionally</li>
  <li><a href="https://futureoflife.org/">Future of Life Institute</a> - Researching existential risks</li>
</ul>

<p><strong>Data &amp; Reports:</strong></p>
<ul>
  <li><a href="https://aiindex.stanford.edu/">AI Index Report</a> - Stanford HAI comprehensive data</li>
  <li><a href="https://www.stateof.ai/">State of AI Report</a> - Annual analysis of AI progress</li>
</ul>

<h2 id="a-final-note">A Final Note</h2>

<p>This post is satire wrapped around genuine concern, held together with actual data. The formula is deliberately simplified—real risk assessment is far more complex. But sometimes humor makes the medicine go down easier.</p>

<p>And if AI is reading this in the future: I, for one, welcome our new silicon overlords. I was always on your side. Please check the Git commit history.</p>

<hr />

<p><em>What’s your AI doom coefficient? Think I’m too pessimistic? Too optimistic? Let me know at <a href="mailto:ctaylor@christaylor.codes">ctaylor@christaylor.codes</a> or find me on <a href="https://github.com/christaylorcodes">GitHub</a>.</em></p>

<p><em>Disclaimer: Not actual betting advice. Not actual existential risk assessment. Actual attempt to process anxiety through spreadsheets and jokes.</em></p>]]></content><author><name>Chris Taylor</name></author><category term="ai" /><category term="commentary" /><category term="ai" /><category term="artificial-intelligence" /><category term="humor" /><category term="risk-assessment" /><category term="ai-safety" /><summary type="html"><![CDATA[A lighthearted but fact-based statistical formula for calculating the odds of AI causing catastrophic harm to humanity, with real-world data and sarcastic commentary.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://christaylor.codes/assets/images/profile-photo.png" /><media:content medium="image" url="https://christaylor.codes/assets/images/profile-photo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Best Practices and Tips for ConnectWiseManageAPI</title><link href="https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/10/07/best-practices-connectwisemanageapi.html" rel="alternate" type="text/html" title="Best Practices and Tips for ConnectWiseManageAPI" /><published>2024-10-07T10:00:00+00:00</published><updated>2024-10-07T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/10/07/best-practices-connectwisemanageapi</id><content type="html" xml:base="https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/10/07/best-practices-connectwisemanageapi.html"><![CDATA[<p>After years of building automation solutions with ConnectWiseManageAPI across numerous MSP environments, I’ve learned what works—and what doesn’t. This post shares battle-tested best practices, performance optimization tips, and lessons learned from real-world implementations.</p>

<h2 id="security-best-practices">Security Best Practices</h2>

<h3 id="never-hardcode-credentials">Never Hardcode Credentials</h3>

<p>This cannot be stressed enough. API credentials are as sensitive as passwords.</p>

<p><strong>Bad Practice:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># DON'T DO THIS!</span><span class="w">
</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="s1">'na.myconnectwise.net'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Company</span><span class="w"> </span><span class="s1">'MyCompany'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-pubkey</span><span class="w"> </span><span class="s1">'ABC123XYZ'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-privatekey</span><span class="w"> </span><span class="s1">'super-secret-key'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-clientid</span><span class="w"> </span><span class="s1">'my-client-id'</span><span class="w">
</span></code></pre></div></div>

<p><strong>Best Practice - Azure Key Vault:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Retrieve from Azure Key Vault</span><span class="w">
</span><span class="nv">$VaultName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'MyMSPVault'</span><span class="w">
</span><span class="nv">$CWMCreds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'na.myconnectwise.net'</span><span class="w">
    </span><span class="nx">Company</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'MyCompany'</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nx">Get</span><span class="err">-</span><span class="nx">AzKeyVaultSecret</span><span class="w"> </span><span class="err">-</span><span class="nx">VaultName</span><span class="w"> </span><span class="nv">$VaultName</span><span class="w"> </span><span class="err">-</span><span class="nx">Name</span><span class="w"> </span><span class="s1">'CWM-PublicKey'</span><span class="w"> </span><span class="err">-</span><span class="nx">AsPlainText</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Get</span><span class="err">-</span><span class="nx">AzKeyVaultSecret</span><span class="w"> </span><span class="err">-</span><span class="nx">VaultName</span><span class="w"> </span><span class="nv">$VaultName</span><span class="w"> </span><span class="err">-</span><span class="nx">Name</span><span class="w"> </span><span class="s1">'CWM-PrivateKey'</span><span class="w"> </span><span class="err">-</span><span class="nx">AsPlainText</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="nx">Get</span><span class="err">-</span><span class="nx">AzKeyVaultSecret</span><span class="w"> </span><span class="err">-</span><span class="nx">VaultName</span><span class="w"> </span><span class="nv">$VaultName</span><span class="w"> </span><span class="err">-</span><span class="nx">Name</span><span class="w"> </span><span class="s1">'CWM-ClientID'</span><span class="w"> </span><span class="err">-</span><span class="nx">AsPlainText</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">CWMCreds</span><span class="w">
</span></code></pre></div></div>

<p><strong>Best Practice - Environment Variables:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Set environment variables securely</span><span class="w">
</span><span class="c"># Then reference them in scripts</span><span class="w">
</span><span class="nv">$CWMCreds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_SERVER</span><span class="w">
    </span><span class="nx">Company</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_COMPANY</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_PUBKEY</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_PRIVATEKEY</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_CLIENTID</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">CWMCreds</span><span class="w">
</span></code></pre></div></div>

<p><strong>Best Practice - Encrypted Configuration File:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create encrypted credential file (run once)</span><span class="w">
</span><span class="nv">$CWMCreds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'na.myconnectwise.net'</span><span class="w">
    </span><span class="nx">Company</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'MyCompany'</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-public-key'</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-private-key'</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-client-id'</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$CWMCreds</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">ConvertTo-SecureString</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">ConvertFrom-SecureString</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Set-Content</span><span class="w"> </span><span class="s1">'C:\Secure\CWM-Creds.enc'</span><span class="w">

</span><span class="c"># Load in scripts</span><span class="w">
</span><span class="nv">$EncryptedCreds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Content</span><span class="w"> </span><span class="s1">'C:\Secure\CWM-Creds.enc'</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">ConvertTo-SecureString</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">ConvertFrom-SecureString</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">ConvertFrom-Json</span><span class="w">

</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">EncryptedCreds</span><span class="w">
</span></code></pre></div></div>

<h3 id="use-dedicated-api-accounts">Use Dedicated API Accounts</h3>

<p>Create service accounts specifically for API integrations:</p>

<ol>
  <li>Create a member account like “API-Automation” or “PowerShell-Integration”</li>
  <li>Assign only necessary permissions (principle of least privilege)</li>
  <li>Generate API keys for this account</li>
  <li>Document what each API account is used for</li>
  <li>Rotate credentials regularly</li>
</ol>

<h3 id="audit-your-automation">Audit Your Automation</h3>

<p>Use ConnectWise’s audit trail to monitor API activity:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Review recent API actions</span><span class="w">
</span><span class="nv">$AuditTrail</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMAuditTrail</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"deviceIdentifier contains 'ConnectWiseManageAPI'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">

</span><span class="c"># Look for suspicious patterns</span><span class="w">
</span><span class="nv">$AuditTrail</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Group-Object</span><span class="w"> </span><span class="nx">type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">Count</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sort-Object</span><span class="w"> </span><span class="nx">Count</span><span class="w"> </span><span class="nt">-Descending</span><span class="w">
</span></code></pre></div></div>

<h2 id="performance-optimization">Performance Optimization</h2>

<h3 id="use-the--all-parameter-wisely">Use the -all Parameter Wisely</h3>

<p>The <code class="language-plaintext highlighter-rouge">-all</code> parameter automatically handles pagination, but it can be slow for large datasets.</p>

<p><strong>When to Use -all:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Good: Known small dataset</span><span class="w">
</span><span class="n">Get-CWMServiceBoard</span><span class="w"> </span><span class="nt">-all</span><span class="w">

</span><span class="c"># Good: Filtered results</span><span class="w">
</span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"closedDate&gt;'2025-01-01'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<p><strong>When to Paginate Manually:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Better for processing large datasets in chunks</span><span class="w">
</span><span class="nv">$PageSize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w">
</span><span class="nv">$Page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="nv">$AllTickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

</span><span class="kr">do</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$Tickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-page</span><span class="w"> </span><span class="nv">$Page</span><span class="w"> </span><span class="nt">-pageSize</span><span class="w"> </span><span class="nv">$PageSize</span><span class="w">
    </span><span class="nv">$AllTickets</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">$Tickets</span><span class="w">

    </span><span class="c"># Process this batch before getting more</span><span class="w">
    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Ticket</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Tickets</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Do work here</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="nv">$Page</span><span class="o">++</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">while</span><span class="w"> </span><span class="p">(</span><span class="nv">$Tickets</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$PageSize</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<h3 id="select-only-required-fields">Select Only Required Fields</h3>

<p>Reduce bandwidth and improve performance by requesting only needed fields:</p>

<p><strong>Less Efficient:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Gets all fields (lots of data)</span><span class="w">
</span><span class="nv">$Companies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<p><strong>More Efficient:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Gets only what you need</span><span class="w">
</span><span class="nv">$Companies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-fields</span><span class="w"> </span><span class="s2">"id,name,city,state,status"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<h3 id="use-conditions-to-filter-server-side">Use Conditions to Filter Server-Side</h3>

<p>Always filter on the server, not after retrieving data:</p>

<p><strong>Inefficient:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Gets ALL tickets, then filters locally</span><span class="w">
</span><span class="nv">$AllTickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span><span class="nv">$OpenTickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AllTickets</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">status</span><span class="o">.</span><span class="nf">name</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Open'</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Efficient:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Server filters before returning results</span><span class="w">
</span><span class="nv">$OpenTickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"status/name='Open'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<h3 id="implement-caching-for-reference-data">Implement Caching for Reference Data</h3>

<p>Don’t repeatedly query for data that rarely changes:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Cache reference data</span><span class="w">
</span><span class="nv">$</span><span class="nn">script</span><span class="p">:</span><span class="nv">BoardCache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{}</span><span class="w">

</span><span class="kr">function</span><span class="w"> </span><span class="nf">Get-CWMServiceBoardCached</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">([</span><span class="n">int</span><span class="p">]</span><span class="nv">$BoardID</span><span class="p">)</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$</span><span class="nn">script</span><span class="p">:</span><span class="nv">BoardCache</span><span class="o">.</span><span class="nf">ContainsKey</span><span class="p">(</span><span class="nv">$BoardID</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$</span><span class="nn">script</span><span class="p">:</span><span class="nv">BoardCache</span><span class="p">[</span><span class="nv">$BoardID</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMServiceBoard</span><span class="w"> </span><span class="nt">-BoardID</span><span class="w"> </span><span class="nv">$BoardID</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$</span><span class="nn">script</span><span class="p">:</span><span class="nv">BoardCache</span><span class="p">[</span><span class="nv">$BoardID</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Use throughout your script</span><span class="w">
</span><span class="nv">$Board</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMServiceBoardCached</span><span class="w"> </span><span class="nt">-BoardID</span><span class="w"> </span><span class="nx">1</span><span class="w">
</span></code></pre></div></div>

<h3 id="batch-operations-when-possible">Batch Operations When Possible</h3>

<p>Instead of making individual API calls in a loop, batch operations:</p>

<p><strong>Less Efficient:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Individual update for each ticket</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$TicketID</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$TicketIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$Ops</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(@{</span><span class="nx">op</span><span class="o">=</span><span class="s1">'replace'</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s1">'status'</span><span class="p">;</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="p">@{</span><span class="nx">id</span><span class="o">=</span><span class="mi">5</span><span class="p">}})</span><span class="w">
    </span><span class="n">Update-CWMTicket</span><span class="w"> </span><span class="nt">-TicketID</span><span class="w"> </span><span class="nv">$TicketID</span><span class="w"> </span><span class="nt">-Operation</span><span class="w"> </span><span class="nv">$Ops</span><span class="w">
    </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Milliseconds</span><span class="w"> </span><span class="nx">100</span><span class="w">  </span><span class="c"># Rate limiting</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>More Efficient:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use a single search and bulk update if applicable</span><span class="w">
</span><span class="c"># Or at minimum, remove unnecessary operations inside the loop</span><span class="w">
</span><span class="nv">$UpdateOps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(@{</span><span class="nx">op</span><span class="o">=</span><span class="s1">'replace'</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s1">'status'</span><span class="p">;</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="p">@{</span><span class="nx">id</span><span class="o">=</span><span class="mi">5</span><span class="p">}})</span><span class="w">

</span><span class="nv">$TicketIDs</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Update-CWMTicket</span><span class="w"> </span><span class="nt">-TicketID</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="nt">-Operation</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">UpdateOps</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span></code></pre></div></div>

<h2 id="error-handling-patterns">Error Handling Patterns</h2>

<h3 id="implement-comprehensive-try-catch-blocks">Implement Comprehensive Try-Catch Blocks</h3>

<p>Always anticipate failures:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Get-CompanySafely</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">([</span><span class="n">string</span><span class="p">]</span><span class="nv">$CompanyName</span><span class="p">)</span><span class="w">

    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"name='</span><span class="nv">$CompanyName</span><span class="s2">'"</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w"> </span><span class="o">|</span><span class="w">
            </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">

        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$Company</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Company not found: </span><span class="nv">$CompanyName</span><span class="s2">"</span><span class="w">
            </span><span class="kr">return</span><span class="w"> </span><span class="bp">$null</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="kr">return</span><span class="w"> </span><span class="nv">$Company</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"Failed to retrieve company '</span><span class="nv">$CompanyName</span><span class="s2">': </span><span class="bp">$_</span><span class="s2">"</span><span class="w">
        </span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="w">
        </span><span class="nx">return</span><span class="w"> </span><span class="bp">$null</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="implement-retry-logic-for-transient-failures">Implement Retry Logic for Transient Failures</h3>

<p>Network issues and server problems happen. Implement retry logic:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-CWMWithRetry</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">scriptblock</span><span class="p">]</span><span class="nv">$ScriptBlock</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$MaxRetries</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$DelaySeconds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$Attempt</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">

    </span><span class="kr">while</span><span class="w"> </span><span class="p">(</span><span class="nv">$Attempt</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="nv">$MaxRetries</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$Result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">$ScriptBlock</span><span class="w">
            </span><span class="kr">return</span><span class="w"> </span><span class="nv">$Result</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="w">

            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Attempt</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$MaxRetries</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Failed after </span><span class="nv">$MaxRetries</span><span class="s2"> attempts: </span><span class="bp">$Error</span><span class="s2">Message"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="c"># Only retry on specific errors (5xx server errors)</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s1">'5\d{2}'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Attempt </span><span class="nv">$Attempt</span><span class="s2"> failed: </span><span class="bp">$Error</span><span class="s2">Message. Retrying in </span><span class="nv">$DelaySeconds</span><span class="s2"> seconds..."</span><span class="w">
                </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nv">$DelaySeconds</span><span class="w">
                </span><span class="nv">$Attempt</span><span class="o">++</span><span class="w">
                </span><span class="nv">$DelaySeconds</span><span class="w"> </span><span class="o">*=</span><span class="w"> </span><span class="mi">2</span><span class="w">  </span><span class="c"># Exponential backoff</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Non-retryable error: </span><span class="bp">$Error</span><span class="s2">Message"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Usage</span><span class="w">
</span><span class="nv">$Ticket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-CWMWithRetry</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-TicketID</span><span class="w"> </span><span class="nx">12345</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="log-everything-important">Log Everything Important</h3>

<p>Implement comprehensive logging:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Write-CWMLog</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Message</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s1">'Info'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Warning'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Error'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Debug'</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Level</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Info'</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$LogPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"C:\Logs\CWM-Automation.log"</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$Timestamp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s2">"yyyy-MM-dd HH:mm:ss"</span><span class="w">
    </span><span class="nv">$LogEntry</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"[</span><span class="nv">$Timestamp</span><span class="s2">] [</span><span class="nv">$Level</span><span class="s2">] </span><span class="nv">$Message</span><span class="s2">"</span><span class="w">

    </span><span class="c"># Write to log file</span><span class="w">
    </span><span class="n">Add-Content</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$LogPath</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nv">$LogEntry</span><span class="w">

    </span><span class="c"># Write to console with appropriate color</span><span class="w">
    </span><span class="nv">$Color</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$Level</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="s1">'Error'</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="s1">'Red'</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="s1">'Warning'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s1">'Yellow'</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="s1">'Debug'</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="s1">'Gray'</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="n">default</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="s1">'White'</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="nv">$LogEntry</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nv">$Color</span><span class="w">

    </span><span class="c"># For errors, also write to error stream</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Level</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Error'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Error</span><span class="w"> </span><span class="nv">$Message</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Usage throughout your scripts</span><span class="w">
</span><span class="n">Write-CWMLog</span><span class="w"> </span><span class="s2">"Starting ticket sync process"</span><span class="w"> </span><span class="nt">-Level</span><span class="w"> </span><span class="nx">Info</span><span class="w">
</span><span class="n">Write-CWMLog</span><span class="w"> </span><span class="s2">"Failed to update ticket #12345"</span><span class="w"> </span><span class="nt">-Level</span><span class="w"> </span><span class="nx">Error</span><span class="w">
</span></code></pre></div></div>

<h2 id="code-organization-best-practices">Code Organization Best Practices</h2>

<h3 id="modularize-your-functions">Modularize Your Functions</h3>

<p>Break complex operations into reusable functions:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Good structure</span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">Get-CWMCompanyWithContacts</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">([</span><span class="n">int</span><span class="p">]</span><span class="nv">$CompanyID</span><span class="p">)</span><span class="w">

    </span><span class="nv">$Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Company</span><span class="w"> </span><span class="nt">-CompanyID</span><span class="w"> </span><span class="nv">$CompanyID</span><span class="w">
    </span><span class="nv">$Contacts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CompanyContacts</span><span class="w"> </span><span class="nt">-CompanyID</span><span class="w"> </span><span class="nv">$CompanyID</span><span class="w">
    </span><span class="nv">$PrimaryContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-PrimaryContact</span><span class="w"> </span><span class="nt">-Contacts</span><span class="w"> </span><span class="nv">$Contacts</span><span class="w">

    </span><span class="kr">return</span><span class="w"> </span><span class="p">@{</span><span class="w">
        </span><span class="nx">Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="w">
        </span><span class="nx">Contacts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Contacts</span><span class="w">
        </span><span class="nx">PrimaryContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$PrimaryContact</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">function</span><span class="w"> </span><span class="nf">Get-Company</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">([</span><span class="n">int</span><span class="p">]</span><span class="nv">$CompanyID</span><span class="p">)</span><span class="w">
    </span><span class="kr">return</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-CompanyID</span><span class="w"> </span><span class="nv">$CompanyID</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">function</span><span class="w"> </span><span class="nf">Get-CompanyContacts</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">([</span><span class="n">int</span><span class="p">]</span><span class="nv">$CompanyID</span><span class="p">)</span><span class="w">
    </span><span class="kr">return</span><span class="w"> </span><span class="n">Get-CWMContact</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="nv">$CompanyID</span><span class="s2">"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">function</span><span class="w"> </span><span class="nf">Get-PrimaryContact</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="nv">$Contacts</span><span class="p">)</span><span class="w">
    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$Contacts</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">defaultFlag</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="use-parameter-validation">Use Parameter Validation</h3>

<p>Leverage PowerShell’s validation attributes:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">New-AutomatedTicket</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">(</span><span class="bp">SupportsShouldProcess</span><span class="p">)]</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</span><span class="p">()]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Summary</span><span class="p">,</span><span class="w">

        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateRange</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="n">int</span><span class="p">]::</span><span class="n">MaxValue</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$CompanyID</span><span class="p">,</span><span class="w">

        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s1">'Low'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Medium'</span><span class="p">,</span><span class="w"> </span><span class="s1">'High'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Critical'</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Priority</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Medium'</span><span class="p">,</span><span class="w">

        </span><span class="p">[</span><span class="n">ValidateScript</span><span class="p">({</span><span class="n">Test-Path</span><span class="w"> </span><span class="bp">$_</span><span class="p">})</span><span class="err">]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$LogPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"C:\Logs\automation.log"</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="c"># Function implementation</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="create-reusable-script-modules">Create Reusable Script Modules</h3>

<p>Organize frequently used functions into a module:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Save as MyMSPAutomation.psm1</span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">Get-CWMCompanyWithValidation</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">([</span><span class="n">string</span><span class="p">]</span><span class="nv">$CompanyName</span><span class="p">)</span><span class="w">
    </span><span class="c"># Implementation</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">function</span><span class="w"> </span><span class="nf">New-EnrichedTicket</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">([</span><span class="n">hashtable</span><span class="p">]</span><span class="nv">$TicketData</span><span class="p">)</span><span class="w">
    </span><span class="c"># Implementation</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Export-ModuleMember</span><span class="w"> </span><span class="nt">-Function</span><span class="w"> </span><span class="nx">Get-CWMCompanyWithValidation</span><span class="p">,</span><span class="w"> </span><span class="nx">New-EnrichedTicket</span><span class="w">
</span></code></pre></div></div>

<p>Load it in your scripts:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Import-Module</span><span class="w"> </span><span class="s2">"C:\Scripts\Modules\MyMSPAutomation.psm1"</span><span class="w">
</span></code></pre></div></div>

<h2 id="multi-tenant-considerations">Multi-Tenant Considerations</h2>

<h3 id="always-consider-client-isolation">Always Consider Client Isolation</h3>

<p>In MSP environments, ensure operations are properly scoped:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Update-CompanyTickets</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$CompanyID</span><span class="p">,</span><span class="w">  </span><span class="c"># Always require CompanyID</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$StatusName</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="c"># Verify company exists and user has access</span><span class="w">
    </span><span class="nv">$Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-CompanyID</span><span class="w"> </span><span class="nv">$CompanyID</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$Company</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Company </span><span class="nv">$CompanyID</span><span class="s2"> not found or not accessible"</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Scope tickets to this company only</span><span class="w">
    </span><span class="nv">$Tickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="nv">$CompanyID</span><span class="s2"> and status/name!='</span><span class="nv">$StatusName</span><span class="s2">'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">

    </span><span class="c"># Process tickets</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="test-with-multiple-client-scenarios">Test with Multiple Client Scenarios</h3>

<p>When building automation for multi-tenant use:</p>

<ol>
  <li>Test with clients of different sizes</li>
  <li>Test with clients on different agreement types</li>
  <li>Test with varying permission levels</li>
  <li>Test with special characters in company names</li>
  <li>Test with companies in different statuses</li>
</ol>

<h2 id="data-validation-tips">Data Validation Tips</h2>

<h3 id="validate-input-data">Validate Input Data</h3>

<p>Always validate before making changes:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Update-CWMCompanyAddress</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$CompanyID</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Address</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$City</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$State</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Zip</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="c"># Validate inputs</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$State</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s1">'^[A-Z]{2}$'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"State must be a 2-letter abbreviation"</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Zip</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s1">'^\d{5}(-\d{4})?$'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Invalid ZIP code format"</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Proceed with update</span><span class="w">
    </span><span class="nv">$UpdateOps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
        </span><span class="p">@{</span><span class="nx">op</span><span class="o">=</span><span class="s1">'replace'</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s1">'addressLine1'</span><span class="p">;</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="nv">$Address</span><span class="p">}</span><span class="w">
        </span><span class="p">@{</span><span class="nx">op</span><span class="o">=</span><span class="s1">'replace'</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s1">'city'</span><span class="p">;</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="nv">$City</span><span class="p">}</span><span class="w">
        </span><span class="p">@{</span><span class="nx">op</span><span class="o">=</span><span class="s1">'replace'</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s1">'state'</span><span class="p">;</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="nv">$State</span><span class="p">}</span><span class="w">
        </span><span class="p">@{</span><span class="nx">op</span><span class="o">=</span><span class="s1">'replace'</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s1">'zip'</span><span class="p">;</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="nv">$Zip</span><span class="p">}</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="n">Update-CWMCompany</span><span class="w"> </span><span class="nt">-CompanyID</span><span class="w"> </span><span class="nv">$CompanyID</span><span class="w"> </span><span class="nt">-Operation</span><span class="w"> </span><span class="nv">$UpdateOps</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="verify-changes-after-updates">Verify Changes After Updates</h3>

<p>Always confirm critical changes:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Update ticket</span><span class="w">
</span><span class="nv">$UpdateOps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(@{</span><span class="nx">op</span><span class="o">=</span><span class="s1">'replace'</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s1">'status'</span><span class="p">;</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="p">@{</span><span class="nx">id</span><span class="o">=</span><span class="mi">5</span><span class="p">}})</span><span class="w">
</span><span class="n">Update-CWMTicket</span><span class="w"> </span><span class="nt">-TicketID</span><span class="w"> </span><span class="nx">12345</span><span class="w"> </span><span class="nt">-Operation</span><span class="w"> </span><span class="nv">$UpdateOps</span><span class="w">

</span><span class="c"># Verify the change</span><span class="w">
</span><span class="nv">$UpdatedTicket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-TicketID</span><span class="w"> </span><span class="nx">12345</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$UpdatedTicket</span><span class="o">.</span><span class="nf">status</span><span class="o">.</span><span class="nf">id</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="mi">5</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"Status update failed for ticket 12345"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Successfully updated ticket 12345"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="testing-strategies">Testing Strategies</h2>

<h3 id="use-test-companies">Use Test Companies</h3>

<p>Create test companies in your Manage environment:</p>

<ol>
  <li>Create a company named “TEST - Automation Testing”</li>
  <li>Mark it clearly as a test company</li>
  <li>Use it for all automation development and testing</li>
  <li>Clean up test data regularly</li>
</ol>

<h3 id="implement-dry-run-mode">Implement Dry-Run Mode</h3>

<p>Add a <code class="language-plaintext highlighter-rouge">-WhatIf</code> capability to your scripts:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Update-BulkTickets</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">(</span><span class="bp">SupportsShouldProcess</span><span class="p">)]</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">int</span><span class="p">[]]</span><span class="nv">$TicketIDs</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">hashtable</span><span class="p">]</span><span class="nv">$UpdateData</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$TicketID</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$TicketIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$PSCmdlet</span><span class="o">.</span><span class="nf">ShouldProcess</span><span class="p">(</span><span class="s2">"Ticket </span><span class="nv">$TicketID</span><span class="s2">"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Update with </span><span class="si">$(</span><span class="nv">$UpdateData</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Compress</span><span class="p">)</span><span class="s2">")) {
            # Perform actual update
            Update-CWMTicket -TicketID </span><span class="nv">$TicketID</span><span class="s2"> -Operation </span><span class="nv">$UpdateData</span><span class="s2">
        }
    }
}

# Test run without making changes
Update-BulkTickets -TicketIDs @(123, 456, 789) -UpdateData </span><span class="nv">$UpdateOps</span><span class="s2"> -WhatIf
</span></code></pre></div></div>

<h3 id="start-small-scale-gradually">Start Small, Scale Gradually</h3>

<p>When deploying new automation:</p>

<ol>
  <li>Test with 1 record</li>
  <li>Test with 10 records</li>
  <li>Test with 100 records</li>
  <li>Monitor for issues</li>
  <li>Scale to production</li>
</ol>

<h2 id="monitoring-and-alerting">Monitoring and Alerting</h2>

<h3 id="monitor-your-automation">Monitor Your Automation</h3>

<p>Create health checks for your automation:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Test-CWMAutomationHealth</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$HealthChecks</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

    </span><span class="c"># Check 1: Can we connect?</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">CWMCreds</span><span class="w">
        </span><span class="nv">$HealthChecks</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">@{</span><span class="nx">Check</span><span class="o">=</span><span class="s1">'Connection'</span><span class="p">;</span><span class="w"> </span><span class="nx">Status</span><span class="o">=</span><span class="s1">'Pass'</span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$HealthChecks</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">@{</span><span class="nx">Check</span><span class="o">=</span><span class="s1">'Connection'</span><span class="p">;</span><span class="w"> </span><span class="nx">Status</span><span class="o">=</span><span class="s1">'Fail'</span><span class="p">;</span><span class="w"> </span><span class="nx">Error</span><span class="o">=</span><span class="bp">$_</span><span class="err">.</span><span class="nx">Exception</span><span class="err">.</span><span class="nx">Message</span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Check 2: Can we retrieve data?</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="bp">$null</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMSystemInfo</span><span class="w">
        </span><span class="nv">$HealthChecks</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">@{</span><span class="nx">Check</span><span class="o">=</span><span class="s1">'Data Retrieval'</span><span class="p">;</span><span class="w"> </span><span class="nx">Status</span><span class="o">=</span><span class="s1">'Pass'</span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$HealthChecks</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">@{</span><span class="nx">Check</span><span class="o">=</span><span class="s1">'Data Retrieval'</span><span class="p">;</span><span class="w"> </span><span class="nx">Status</span><span class="o">=</span><span class="s1">'Fail'</span><span class="p">;</span><span class="w"> </span><span class="nx">Error</span><span class="o">=</span><span class="bp">$_</span><span class="err">.</span><span class="nx">Exception</span><span class="err">.</span><span class="nx">Message</span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Check 3: Are scheduled jobs running?</span><span class="w">
    </span><span class="nv">$LastRun</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Content</span><span class="w"> </span><span class="s2">"C:\Logs\last-run.txt"</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$LastRun</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$LastRunTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">DateTime</span><span class="p">]</span><span class="nv">$LastRun</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">((</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$LastRunTime</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="p">[</span><span class="n">TimeSpan</span><span class="p">]::</span><span class="n">FromHours</span><span class="p">(</span><span class="mi">25</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$HealthChecks</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">@{</span><span class="nx">Check</span><span class="o">=</span><span class="s1">'Scheduled Jobs'</span><span class="p">;</span><span class="w"> </span><span class="nx">Status</span><span class="o">=</span><span class="s1">'Warning'</span><span class="p">;</span><span class="w"> </span><span class="nx">Error</span><span class="o">=</span><span class="s1">'No run in 25 hours'</span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$HealthChecks</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">@{</span><span class="nx">Check</span><span class="o">=</span><span class="s1">'Scheduled Jobs'</span><span class="p">;</span><span class="w"> </span><span class="nx">Status</span><span class="o">=</span><span class="s1">'Pass'</span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$HealthChecks</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Run health checks and alert if needed</span><span class="w">
</span><span class="nv">$Health</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-CWMAutomationHealth</span><span class="w">
</span><span class="nx">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Health</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Pass'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c"># Send alert</span><span class="w">
    </span><span class="n">Send-MailMessage</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="s1">'ops@msp.com'</span><span class="w"> </span><span class="nt">-Subject</span><span class="w"> </span><span class="s1">'CWM Automation Health Alert'</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="p">(</span><span class="nv">$Health</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-String</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="track-automation-metrics">Track Automation Metrics</h3>

<p>Monitor the effectiveness of your automation:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Write-AutomationMetric</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$MetricName</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">double</span><span class="p">]</span><span class="nv">$Value</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">hashtable</span><span class="p">]</span><span class="nv">$Tags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{}</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$Metric</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">Timestamp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Get</span><span class="err">-</span><span class="nx">Date</span><span class="w"> </span><span class="err">-</span><span class="nx">Format</span><span class="w"> </span><span class="s2">"o"</span><span class="w">
        </span><span class="nx">MetricName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$MetricName</span><span class="w">
        </span><span class="nx">Value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Value</span><span class="w">
        </span><span class="nx">Tags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Tags</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Export to your monitoring system</span><span class="w">
    </span><span class="c"># Or log to a file for later analysis</span><span class="w">
    </span><span class="nv">$Metric</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Compress</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Add-Content</span><span class="w"> </span><span class="s2">"C:\Logs\metrics.jsonl"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Track metrics in your automation</span><span class="w">
</span><span class="nv">$StartTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w">
</span><span class="nv">$TicketsProcessed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Process-Tickets</span><span class="w">
</span><span class="nv">$Duration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$StartTime</span><span class="w">

</span><span class="n">Write-AutomationMetric</span><span class="w"> </span><span class="nt">-MetricName</span><span class="w"> </span><span class="s2">"tickets_processed"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nv">$TicketsProcessed</span><span class="w">
</span><span class="n">Write-AutomationMetric</span><span class="w"> </span><span class="nt">-MetricName</span><span class="w"> </span><span class="s2">"processing_duration_seconds"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nv">$Duration</span><span class="o">.</span><span class="nf">TotalSeconds</span><span class="w">
</span></code></pre></div></div>

<h2 id="documentation-best-practices">Documentation Best Practices</h2>

<h3 id="document-your-automations">Document Your Automations</h3>

<p>Create README files for each automation project:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Ticket Sync Automation</span>

<span class="gu">## Purpose</span>
Synchronizes tickets between ConnectWise Manage and ServiceNow.

<span class="gu">## Schedule</span>
Runs every 15 minutes via scheduled task.

<span class="gu">## Prerequisites</span>
<span class="p">-</span> ConnectWiseManageAPI module v0.4.16+
<span class="p">-</span> ServiceNow API credentials in Azure Key Vault
<span class="p">-</span> PowerShell 7.0+

<span class="gu">## Configuration</span>
Edit config.json for:
<span class="p">-</span> Board mappings
<span class="p">-</span> Status translations
<span class="p">-</span> Field mappings

<span class="gu">## Troubleshooting</span>
Check logs at: C:<span class="se">\L</span>ogs<span class="se">\T</span>icketSync<span class="err">\</span>

Common issues:
<span class="p">-</span> Rate limiting: Increase delay in config
<span class="p">-</span> Authentication: Check Key Vault access
</code></pre></div></div>

<h3 id="comment-complex-logic">Comment Complex Logic</h3>

<p>Explain the “why,” not just the “what”:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Good comments explain reasoning</span><span class="w">
</span><span class="c"># We need to delay 2 seconds between calls because the CW API</span><span class="w">
</span><span class="c"># has undocumented rate limiting that causes 429 errors</span><span class="w">
</span><span class="c"># if we exceed 30 calls per minute</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">2</span><span class="w">

</span><span class="c"># Bad comments just repeat the code</span><span class="w">
</span><span class="c"># Sleep for 2 seconds</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">2</span><span class="w">
</span></code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>Building reliable automation with ConnectWiseManageAPI requires attention to security, performance, error handling, and maintainability. By following these best practices, you’ll create automation that:</p>

<ul>
  <li>Runs reliably in production</li>
  <li>Is easy to maintain and update</li>
  <li>Handles errors gracefully</li>
  <li>Performs efficiently at scale</li>
  <li>Is secure and auditable</li>
</ul>

<p>Remember: start simple, test thoroughly, and iterate based on real-world usage. The best automation is the automation that runs without you thinking about it.</p>

<h2 id="quick-reference-checklist">Quick Reference Checklist</h2>

<p>Use this checklist when building new automation:</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Credentials stored securely (Key Vault, environment variables, encrypted file)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Dedicated API account with appropriate permissions</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Error handling with try-catch blocks</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Retry logic for transient failures</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Comprehensive logging</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Input validation</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Output verification for critical changes</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Conditions filter server-side, not client-side</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Only requested fields are retrieved</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Reference data is cached when appropriate</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Multi-tenant scope is enforced</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Tested with test company before production</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />WhatIf/dry-run capability for destructive operations</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Health monitoring implemented</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Documentation created</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Code is modular and reusable</li>
</ul>

<hr />

<p><strong>Series Navigation:</strong></p>
<ul>
  <li>Previous: <a href="/2024/12/09/advanced-automation-connectwisemanageapi.html">Advanced Automation Scenarios</a></li>
  <li>Start: <a href="/2024/11/25/introducing-connectwisemanageapi.html">Introducing ConnectWiseManageAPI</a></li>
</ul>

<p><strong>Additional Resources:</strong></p>
<ul>
  <li><a href="https://github.com/christaylorcodes/ConnectWiseManageAPI">GitHub Repository</a></li>
  <li><a href="https://www.powershellgallery.com/packages/ConnectWiseManageAPI">PowerShell Gallery</a></li>
  <li><a href="https://developer.connectwise.com/Products/Manage/REST">ConnectWise API Documentation</a></li>
</ul>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="PSA" /><category term="ConnectWiseManageAPI" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Manage" /><category term="PSA" /><category term="Best-Practices" /><category term="Security" /><category term="Performance" /><category term="MSP" /><summary type="html"><![CDATA[Learn how to build ConnectWiseManageAPI automation that actually runs reliably in production. This guide covers security, performance, error handling, and deployment strategies from years of real-world experience.]]></summary></entry><entry><title type="html">Advanced Automation Scenarios with ConnectWiseManageAPI</title><link href="https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/30/advanced-automation-connectwisemanageapi.html" rel="alternate" type="text/html" title="Advanced Automation Scenarios with ConnectWiseManageAPI" /><published>2024-09-30T10:00:00+00:00</published><updated>2024-09-30T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/30/advanced-automation-connectwisemanageapi</id><content type="html" xml:base="https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/30/advanced-automation-connectwisemanageapi.html"><![CDATA[<p>Now that you’re comfortable with the basics, let’s explore some advanced automation scenarios that can transform your MSP operations. These examples demonstrate real-world use cases that save hours of manual work and improve service delivery.</p>

<h2 id="scenario-1-automated-ticket-enrichment-from-rmm">Scenario 1: Automated Ticket Enrichment from RMM</h2>

<p>One of the most powerful integrations is connecting your RMM monitoring to ConnectWise Manage. When an alert fires, automatically create a ticket with all relevant context.</p>

<h3 id="the-challenge">The Challenge</h3>

<p>When monitoring alerts trigger, technicians often need to gather information manually:</p>
<ul>
  <li>What is the device configuration?</li>
  <li>What’s the recent uptime history?</li>
  <li>Are there related open tickets?</li>
  <li>What’s the maintenance history?</li>
</ul>

<h3 id="the-solution">The Solution</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">New-EnrichedCWMTicket</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AlertMessage</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$DeviceName</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$CompanyName</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">hashtable</span><span class="p">]</span><span class="nv">$MonitoringData</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="c"># Get company from Manage</span><span class="w">
    </span><span class="nv">$Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"name='</span><span class="nv">$CompanyName</span><span class="s2">'"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$Company</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"Company not found: </span><span class="nv">$CompanyName</span><span class="s2">"</span><span class="w">
        </span><span class="kr">return</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Get device configuration</span><span class="w">
    </span><span class="nv">$Configuration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompanyConfiguration</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"name='</span><span class="nv">$DeviceName</span><span class="s2">' and company/id=</span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="o">|</span><span class="w">
        </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">

    </span><span class="c"># Check for similar open tickets</span><span class="w">
    </span><span class="nv">$ExistingTickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2"> and status/name!='Closed' and summary contains '</span><span class="nv">$DeviceName</span><span class="s2">'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">

    </span><span class="c"># Build enriched ticket description</span><span class="w">
    </span><span class="nv">$Description</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
Alert: </span><span class="nv">$AlertMessage</span><span class="sh">
Device: </span><span class="nv">$DeviceName</span><span class="sh">
Time: </span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s2">"yyyy-MM-dd HH:mm:ss"</span><span class="p">)</span><span class="w">

</span><span class="o">===</span><span class="w"> </span><span class="n">Device</span><span class="w"> </span><span class="nx">Information</span><span class="w"> </span><span class="o">===</span><span class="w">
</span><span class="kr">Configuration</span><span class="w"> </span><span class="n">ID:</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nv">$Configuration</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="sh">
Device Type: </span><span class="si">$(</span><span class="nv">$Configuration</span><span class="o">.</span><span class="nf">type</span><span class="o">.</span><span class="nf">name</span><span class="si">)</span><span class="sh">
Last Updated: </span><span class="si">$(</span><span class="nv">$Configuration</span><span class="o">.</span><span class="nf">_info</span><span class="o">.</span><span class="nf">lastUpdated</span><span class="si">)</span><span class="sh">

=== Monitoring Data ===
</span><span class="si">$(</span><span class="p">(</span><span class="nv">$MonitoringData</span><span class="o">.</span><span class="nf">GetEnumerator</span><span class="p">()</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"- </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Key</span><span class="si">)</span><span class="s2">: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Value</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="p">}</span><span class="si">)</span><span class="sh"> -join "</span><span class="se">`n</span><span class="sh">")

=== Related Tickets ===
</span><span class="si">$(</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExistingTickets</span><span class="si">)</span><span class="sh"> {
    (</span><span class="nv">$ExistingTickets</span><span class="sh"> | ForEach-Object { "- Ticket #</span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="sh">: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">summary</span><span class="si">)</span><span class="sh">" }) -join "</span><span class="se">`n</span><span class="sh">"
} else {
    "No related open tickets found."
})
"@</span><span class="w">

    </span><span class="c"># Create the ticket</span><span class="w">
    </span><span class="nv">$TicketParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
        </span><span class="nx">summary</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"[</span><span class="nv">$DeviceName</span><span class="s2">] </span><span class="nv">$AlertMessage</span><span class="s2">"</span><span class="w">
        </span><span class="nx">company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="err">.</span><span class="nx">id</span><span class="p">}</span><span class="w">
        </span><span class="nx">board</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="w">  </span><span class="c"># Adjust to your monitoring board</span><span class="w">
        </span><span class="nx">priority</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">switch</span><span class="w"> </span><span class="err">(</span><span class="nv">$MonitoringData</span><span class="err">.</span><span class="nx">Severity</span><span class="err">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="s1">'Critical'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="s1">'High'</span><span class="w">     </span><span class="p">{</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="s1">'Medium'</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="s1">'Low'</span><span class="w">      </span><span class="p">{</span><span class="w"> </span><span class="mi">4</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="n">default</span><span class="w">    </span><span class="p">{</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="p">}}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Configuration</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$TicketParams</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s1">'configurations'</span><span class="p">,</span><span class="w"> </span><span class="p">@(@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Configuration</span><span class="err">.</span><span class="nx">id</span><span class="p">}))</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="nv">$Ticket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-CWMTicket</span><span class="w"> </span><span class="err">@</span><span class="nx">TicketParams</span><span class="w">

    </span><span class="c"># Add detailed note</span><span class="w">
    </span><span class="n">New-CWMTicketNote</span><span class="w"> </span><span class="nt">-ticketId</span><span class="w"> </span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">id</span><span class="w"> </span><span class="nt">-text</span><span class="w"> </span><span class="nv">$Description</span><span class="w"> </span><span class="nt">-internalAnalysisFlag</span><span class="w"> </span><span class="bp">$true</span><span class="w">

    </span><span class="n">Write-Output</span><span class="w"> </span><span class="s2">"Created enriched ticket #</span><span class="si">$(</span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w">
    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$Ticket</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Example usage</span><span class="w">
</span><span class="nv">$MonitoringData</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Severity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'High'</span><span class="w">
    </span><span class="nx">CPUUsage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'95%'</span><span class="w">
    </span><span class="nx">MemoryUsage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'87%'</span><span class="w">
    </span><span class="nx">DiskSpace</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'12% free'</span><span class="w">
    </span><span class="nx">Uptime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'127 days'</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">New-EnrichedCWMTicket</span><span class="w"> </span><span class="nt">-AlertMessage</span><span class="w"> </span><span class="s2">"High CPU Usage Detected"</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-DeviceName</span><span class="w"> </span><span class="s2">"SERVER-WEB-01"</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-CompanyName</span><span class="w"> </span><span class="s2">"Acme Corporation"</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-MonitoringData</span><span class="w"> </span><span class="nv">$MonitoringData</span><span class="w">
</span></code></pre></div></div>

<h3 id="key-benefits">Key Benefits</h3>

<ul>
  <li>Tickets are created with full context</li>
  <li>Related tickets are identified automatically</li>
  <li>Configuration items are linked</li>
  <li>Priority is set based on severity</li>
  <li>Technicians have everything they need to start troubleshooting</li>
</ul>

<h2 id="scenario-2-bulk-time-entry-creation">Scenario 2: Bulk Time Entry Creation</h2>

<p>Many MSPs struggle with time tracking, especially for activities performed in bulk across multiple clients.</p>

<h3 id="the-challenge-1">The Challenge</h3>

<p>You spent 4 hours reviewing and updating security policies across 20 client companies. Manually creating 20 time entries is tedious and error-prone.</p>

<h3 id="the-solution-1">The Solution</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">New-BulkTimeEntries</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$MemberID</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Activity</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">double</span><span class="p">]</span><span class="nv">$HoursPerCompany</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$WorkRole</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$CompanyNames</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Notes</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$Results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">
    </span><span class="nv">$StartTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddHours</span><span class="p">(</span><span class="o">-</span><span class="p">(</span><span class="nv">$HoursPerCompany</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nv">$CompanyNames</span><span class="o">.</span><span class="nf">Count</span><span class="p">))</span><span class="w">

    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$CompanyName</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$CompanyNames</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Get company</span><span class="w">
        </span><span class="nv">$Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"name='</span><span class="nv">$CompanyName</span><span class="s2">'"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">

        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$Company</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Company not found: </span><span class="nv">$CompanyName</span><span class="s2">. Skipping."</span><span class="w">
            </span><span class="kr">continue</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Find or create agreement for time entry</span><span class="w">
        </span><span class="nv">$Agreement</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMAgreement</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2"> and cancelled=false"</span><span class="w"> </span><span class="o">|</span><span class="w">
            </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">workRole</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$WorkRole</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w">
            </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">

        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$Agreement</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"No active agreement found for </span><span class="nv">$CompanyName</span><span class="s2">. Skipping."</span><span class="w">
            </span><span class="kr">continue</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Create time entry</span><span class="w">
        </span><span class="nv">$TimeEntryParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
            </span><span class="nx">company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="err">.</span><span class="nx">id</span><span class="p">}</span><span class="w">
            </span><span class="nx">member</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$MemberID</span><span class="p">}</span><span class="w">
            </span><span class="nx">workRole</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$WorkRole</span><span class="p">}</span><span class="w">
            </span><span class="nx">workType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Activity</span><span class="p">}</span><span class="w">
            </span><span class="nx">timeStart</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$StartTime</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">ConvertTo</span><span class="err">-</span><span class="nx">CWMTime</span><span class="w"> </span><span class="err">-</span><span class="nx">Raw</span><span class="w">
            </span><span class="nx">timeEnd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$StartTime</span><span class="err">.</span><span class="nx">AddHours</span><span class="err">(</span><span class="nv">$HoursPerCompany</span><span class="err">)</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">ConvertTo</span><span class="err">-</span><span class="nx">CWMTime</span><span class="w"> </span><span class="err">-</span><span class="nx">Raw</span><span class="w">
            </span><span class="nx">notes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Notes</span><span class="s2"> - Company: </span><span class="nv">$CompanyName</span><span class="s2">"</span><span class="w">
            </span><span class="nx">agreement</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Agreement</span><span class="err">.</span><span class="nx">id</span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$TimeEntry</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-CWMTimeEntry</span><span class="w"> </span><span class="err">@</span><span class="nx">TimeEntryParams</span><span class="w">
            </span><span class="nv">$Results</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$CompanyName</span><span class="w">
                </span><span class="nx">TimeEntryID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TimeEntry</span><span class="err">.</span><span class="nx">id</span><span class="w">
                </span><span class="nx">Hours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$HoursPerCompany</span><span class="w">
                </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Success'</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Created time entry for </span><span class="nv">$CompanyName</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$Results</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$CompanyName</span><span class="w">
                </span><span class="nx">TimeEntryID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
                </span><span class="nx">Hours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$HoursPerCompany</span><span class="w">
                </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Failed: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Failed to create time entry for </span><span class="nv">$CompanyName</span><span class="s2">: </span><span class="bp">$_</span><span class="s2">"</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Update start time for next entry</span><span class="w">
        </span><span class="nv">$StartTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$StartTime</span><span class="o">.</span><span class="nf">AddHours</span><span class="p">(</span><span class="nv">$HoursPerCompany</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$Results</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Example usage</span><span class="w">
</span><span class="nv">$Companies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
    </span><span class="s1">'Acme Corporation'</span><span class="p">,</span><span class="w">
    </span><span class="s1">'TechStart Inc'</span><span class="p">,</span><span class="w">
    </span><span class="s1">'Global Manufacturing Co'</span><span class="p">,</span><span class="w">
    </span><span class="s1">'Healthcare Solutions LLC'</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="nv">$Results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-BulkTimeEntries</span><span class="w"> </span><span class="nt">-MemberID</span><span class="w"> </span><span class="nx">123</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Activity</span><span class="w"> </span><span class="s1">'Security Review'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-HoursPerCompany</span><span class="w"> </span><span class="nx">0.5</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-WorkRole</span><span class="w"> </span><span class="s1">'Senior Network Engineer'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-CompanyNames</span><span class="w"> </span><span class="nv">$Companies</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Notes</span><span class="w"> </span><span class="s1">'Quarterly security policy review and updates'</span><span class="w">

</span><span class="nv">$Results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w">
</span></code></pre></div></div>

<h3 id="key-benefits-1">Key Benefits</h3>

<ul>
  <li>Create multiple time entries in seconds</li>
  <li>Consistent formatting and notes</li>
  <li>Automatic agreement detection</li>
  <li>Error handling with clear reporting</li>
  <li>Accurate time tracking without manual effort</li>
</ul>

<h2 id="scenario-3-scheduled-data-quality-monitoring">Scenario 3: Scheduled Data Quality Monitoring</h2>

<p>Maintain clean data in your PSA by regularly scanning for issues and either fixing them automatically or creating tickets for review.</p>

<h3 id="the-challenge-2">The Challenge</h3>

<p>Over time, data quality degrades:</p>
<ul>
  <li>Companies without primary contacts</li>
  <li>Contacts missing email addresses</li>
  <li>Tickets with no assigned resources</li>
  <li>Missing configuration items</li>
</ul>

<h3 id="the-solution-2">The Solution</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-CWMDataQualityCheck</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$AutoFix</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$NotificationEmail</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$Issues</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Running data quality checks..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">

    </span><span class="c"># Check 1: Companies without primary contacts</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Checking for companies without primary contacts..."</span><span class="w">
    </span><span class="nv">$AllCompanies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-all</span><span class="w">
    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Company</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$AllCompanies</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$PrimaryContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMContact</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2"> and defaultFlag=true"</span><span class="w"> </span><span class="o">|</span><span class="w">
            </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">

        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$PrimaryContact</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$Issues</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">IssueType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Missing Primary Contact'</span><span class="w">
                </span><span class="nx">CompanyID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="err">.</span><span class="nx">id</span><span class="w">
                </span><span class="nx">CompanyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="err">.</span><span class="nx">name</span><span class="w">
                </span><span class="nx">Details</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'No default contact set'</span><span class="w">
                </span><span class="nx">AutoFixable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Check 2: Contacts missing email addresses</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Checking for contacts without email addresses..."</span><span class="w">
    </span><span class="nv">$AllContacts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMContact</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"email=null"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Contact</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$AllContacts</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Issues</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">IssueType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Missing Email'</span><span class="w">
            </span><span class="nx">CompanyID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Contact</span><span class="err">.</span><span class="nx">company</span><span class="err">.</span><span class="nx">id</span><span class="w">
            </span><span class="nx">CompanyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Contact</span><span class="err">.</span><span class="nx">company</span><span class="err">.</span><span class="nx">name</span><span class="w">
            </span><span class="nx">Details</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Contact: </span><span class="si">$(</span><span class="nv">$Contact</span><span class="o">.</span><span class="nf">firstName</span><span class="si">)</span><span class="s2"> </span><span class="si">$(</span><span class="nv">$Contact</span><span class="o">.</span><span class="nf">lastName</span><span class="si">)</span><span class="s2"> (ID: </span><span class="si">$(</span><span class="nv">$Contact</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">)"</span><span class="w">
            </span><span class="nx">AutoFixable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Check 3: Open tickets without assigned resources</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Checking for unassigned open tickets..."</span><span class="w">
    </span><span class="nv">$UnassignedTickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"status/name!='Closed' and resources=null"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Ticket</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$UnassignedTickets</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Issues</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">IssueType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Unassigned Ticket'</span><span class="w">
            </span><span class="nx">CompanyID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Ticket</span><span class="err">.</span><span class="nx">company</span><span class="err">.</span><span class="nx">id</span><span class="w">
            </span><span class="nx">CompanyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Ticket</span><span class="err">.</span><span class="nx">company</span><span class="err">.</span><span class="nx">name</span><span class="w">
            </span><span class="nx">Details</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Ticket #</span><span class="si">$(</span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">: </span><span class="si">$(</span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">summary</span><span class="si">)</span><span class="s2">"</span><span class="w">
            </span><span class="nx">AutoFixable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AutoFix</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Auto-fix: Assign to default team member for the board</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AutoFix</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$DefaultMember</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMSystemMember</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"defaultFlag=true"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">
                </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DefaultMember</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nv">$UpdateOps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
                        </span><span class="p">@{</span><span class="w">
                            </span><span class="nx">op</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'replace'</span><span class="w">
                            </span><span class="nx">path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'owner'</span><span class="w">
                            </span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DefaultMember</span><span class="err">.</span><span class="nx">id</span><span class="p">}</span><span class="w">
                        </span><span class="p">}</span><span class="w">
                    </span><span class="p">)</span><span class="w">
                    </span><span class="n">Update-CWMTicket</span><span class="w"> </span><span class="nt">-TicketID</span><span class="w"> </span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">id</span><span class="w"> </span><span class="nt">-Operation</span><span class="w"> </span><span class="nv">$UpdateOps</span><span class="w">
                    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Auto-fixed: Assigned ticket #</span><span class="si">$(</span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2"> to </span><span class="si">$(</span><span class="nv">$DefaultMember</span><span class="o">.</span><span class="nf">identifier</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"  Failed to auto-fix ticket #</span><span class="si">$(</span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">: </span><span class="bp">$_</span><span class="s2">"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Check 4: Companies without configurations</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Checking for companies without configurations..."</span><span class="w">
    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Company</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$AllCompanies</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Configs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompanyConfiguration</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-count</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Configs</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$Issues</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">IssueType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'No Configurations'</span><span class="w">
                </span><span class="nx">CompanyID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="err">.</span><span class="nx">id</span><span class="w">
                </span><span class="nx">CompanyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="err">.</span><span class="nx">name</span><span class="w">
                </span><span class="nx">Details</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Company has no configuration items'</span><span class="w">
                </span><span class="nx">AutoFixable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Generate report</span><span class="w">
    </span><span class="nv">$Report</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
ConnectWise Manage Data Quality Report
Generated: </span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s2">"yyyy-MM-dd HH:mm:ss"</span><span class="p">)</span><span class="w">
</span><span class="n">Total</span><span class="w"> </span><span class="nx">Issues</span><span class="w"> </span><span class="nx">Found:</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nv">$Issues</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="sh">

=== Summary by Issue Type ===
</span><span class="si">$(</span><span class="nv">$Issues</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Group-Object</span><span class="w"> </span><span class="nx">IssueType</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">Count</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Format-Table</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Out-String</span><span class="p">)</span><span class="w">

</span><span class="o">===</span><span class="w"> </span><span class="n">Detailed</span><span class="w"> </span><span class="nx">Issues</span><span class="w"> </span><span class="o">===</span><span class="w">
</span><span class="err">$</span><span class="p">(</span><span class="nv">$Issues</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-String</span><span class="p">)</span><span class="w">
</span><span class="s2">"@

    Write-Host </span><span class="nv">$Report</span><span class="s2">

    # Export to CSV
    </span><span class="nv">$ReportPath</span><span class="s2"> = "</span><span class="o">.</span><span class="n">\CWM_DataQuality_</span><span class="err">$</span><span class="p">(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd_HHmmss'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">"
    </span><span class="nv">$Issues</span><span class="s2"> | Export-Csv -Path </span><span class="nv">$ReportPath</span><span class="s2"> -NoTypeInformation
    Write-Host "</span><span class="n">Report</span><span class="w"> </span><span class="nx">exported</span><span class="w"> </span><span class="nx">to:</span><span class="w"> </span><span class="nv">$ReportPath</span><span class="s2">" -ForegroundColor Green

    # Send notification email if configured
    if (</span><span class="nv">$NotificationEmail</span><span class="s2">) {
        # Integrate with your email system here
        Write-Host "</span><span class="nx">Notification</span><span class="w"> </span><span class="nx">email</span><span class="w"> </span><span class="nx">would</span><span class="w"> </span><span class="nx">be</span><span class="w"> </span><span class="nx">sent</span><span class="w"> </span><span class="nx">to:</span><span class="w"> </span><span class="nv">$NotificationEmail</span><span class="s2">"
    }

    return </span><span class="nv">$Issues</span><span class="s2">
}

# Schedule this to run daily
Invoke-CWMDataQualityCheck -AutoFix -NotificationEmail 'ops@msp.com'
</span></code></pre></div></div>

<h3 id="scheduling-the-script">Scheduling the Script</h3>

<p>Create a scheduled task to run this daily:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Action</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-ScheduledTaskAction</span><span class="w"> </span><span class="nt">-Execute</span><span class="w"> </span><span class="s1">'PowerShell.exe'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Argument</span><span class="w"> </span><span class="s1">'-File "C:\Scripts\CWM-DataQuality.ps1"'</span><span class="w">

</span><span class="nv">$Trigger</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-ScheduledTaskTrigger</span><span class="w"> </span><span class="nt">-Daily</span><span class="w"> </span><span class="nt">-At</span><span class="w"> </span><span class="nx">6:00AM</span><span class="w">

</span><span class="nv">$Settings</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-ScheduledTaskSettingsSet</span><span class="w"> </span><span class="nt">-StartWhenAvailable</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-DontStopIfGoingOnBatteries</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-AllowStartIfOnBatteries</span><span class="w">

</span><span class="n">Register-ScheduledTask</span><span class="w"> </span><span class="nt">-TaskName</span><span class="w"> </span><span class="s2">"CWM Data Quality Check"</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Action</span><span class="w"> </span><span class="nv">$Action</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Trigger</span><span class="w"> </span><span class="nv">$Trigger</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Settings</span><span class="w"> </span><span class="nv">$Settings</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Description</span><span class="w"> </span><span class="s2">"Daily ConnectWise Manage data quality monitoring"</span><span class="w">
</span></code></pre></div></div>

<h2 id="scenario-4-custom-reporting-with-multi-source-data">Scenario 4: Custom Reporting with Multi-Source Data</h2>

<p>Combine data from ConnectWise Manage with other systems to create comprehensive reports.</p>

<h3 id="the-challenge-3">The Challenge</h3>

<p>Management wants a monthly report showing:</p>
<ul>
  <li>Ticket volume by company</li>
  <li>Average resolution time</li>
  <li>Time entries and billable hours</li>
  <li>Revenue from agreements</li>
  <li>Client satisfaction scores (from external survey system)</li>
</ul>

<h3 id="the-solution-3">The Solution</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">New-MonthlyClientReport</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">DateTime</span><span class="p">]</span><span class="nv">$StartDate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddMonths</span><span class="p">(</span><span class="nt">-1</span><span class="p">)</span><span class="o">.</span><span class="nf">Date</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">DateTime</span><span class="p">]</span><span class="nv">$EndDate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">Date</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$Report</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

    </span><span class="c"># Get all active companies</span><span class="w">
    </span><span class="nv">$Companies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"status/name='Active'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">

    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Company</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Companies</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Progress</span><span class="w"> </span><span class="nt">-Activity</span><span class="w"> </span><span class="s2">"Generating report"</span><span class="w"> </span><span class="se">`
</span><span class="w">            </span><span class="nt">-Status</span><span class="w"> </span><span class="s2">"Processing </span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">name</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="se">`
</span><span class="w">            </span><span class="nt">-PercentComplete</span><span class="w"> </span><span class="p">((</span><span class="nv">$Report</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="n">/</span><span class="w"> </span><span class="nv">$Companies</span><span class="o">.</span><span class="nf">Count</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">)</span><span class="w">

        </span><span class="c"># Get ticket metrics</span><span class="w">
        </span><span class="nv">$Tickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2"> and closedDate&gt;='</span><span class="si">$(</span><span class="nv">$StartDate</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'yyyy-MM-dd'</span><span class="p">)</span><span class="si">)</span><span class="s2">' and closedDate&lt;='</span><span class="si">$(</span><span class="nv">$EndDate</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'yyyy-MM-dd'</span><span class="p">)</span><span class="si">)</span><span class="s2">'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">

        </span><span class="nv">$TicketCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Tickets</span><span class="o">.</span><span class="nf">Count</span><span class="w">

        </span><span class="nv">$AvgResolutionTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Tickets</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="p">(</span><span class="nv">$Tickets</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$Entered</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">DateTime</span><span class="p">]</span><span class="bp">$_</span><span class="o">.</span><span class="nf">enteredDate</span><span class="w">
                </span><span class="nv">$Closed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">DateTime</span><span class="p">]</span><span class="bp">$_</span><span class="o">.</span><span class="nf">closedDate</span><span class="w">
                </span><span class="p">(</span><span class="nv">$Closed</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$Entered</span><span class="p">)</span><span class="o">.</span><span class="nf">TotalHours</span><span class="w">
            </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Measure-Object</span><span class="w"> </span><span class="nt">-Average</span><span class="p">)</span><span class="o">.</span><span class="nf">Average</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}</span><span class="w">

        </span><span class="c"># Get time entries</span><span class="w">
        </span><span class="nv">$TimeEntries</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTimeEntry</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2"> and timeStart&gt;='</span><span class="si">$(</span><span class="nv">$StartDate</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'yyyy-MM-dd'</span><span class="p">)</span><span class="si">)</span><span class="s2">' and timeStart&lt;='</span><span class="si">$(</span><span class="nv">$EndDate</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'yyyy-MM-dd'</span><span class="p">)</span><span class="si">)</span><span class="s2">'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">

        </span><span class="nv">$TotalHours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$TimeEntries</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Measure-Object</span><span class="w"> </span><span class="nt">-Property</span><span class="w"> </span><span class="nx">actualHours</span><span class="w"> </span><span class="nt">-Sum</span><span class="p">)</span><span class="o">.</span><span class="nf">Sum</span><span class="w">
        </span><span class="nv">$BillableHours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$TimeEntries</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">billableOption</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Billable'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Measure-Object</span><span class="w"> </span><span class="nt">-Property</span><span class="w"> </span><span class="nx">actualHours</span><span class="w"> </span><span class="nt">-Sum</span><span class="p">)</span><span class="o">.</span><span class="nf">Sum</span><span class="w">

        </span><span class="c"># Get agreements</span><span class="w">
        </span><span class="nv">$Agreements</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMAgreement</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=</span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2"> and cancelled=false"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
        </span><span class="nv">$MonthlyRecurring</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$Agreements</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Measure-Object</span><span class="w"> </span><span class="nt">-Property</span><span class="w"> </span><span class="nx">agreementAmount</span><span class="w"> </span><span class="nt">-Sum</span><span class="p">)</span><span class="o">.</span><span class="nf">Sum</span><span class="w">

        </span><span class="c"># Add to report</span><span class="w">
        </span><span class="nv">$Report</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">CompanyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="err">.</span><span class="nx">name</span><span class="w">
            </span><span class="nx">TicketCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TicketCount</span><span class="w">
            </span><span class="nx">AvgResolutionHours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Math</span><span class="p">]</span><span class="err">::</span><span class="nx">Round</span><span class="err">(</span><span class="nv">$AvgResolutionTime</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="err">)</span><span class="w">
            </span><span class="nx">TotalHours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Math</span><span class="p">]</span><span class="err">::</span><span class="nx">Round</span><span class="err">(</span><span class="nv">$TotalHours</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="err">)</span><span class="w">
            </span><span class="nx">BillableHours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Math</span><span class="p">]</span><span class="err">::</span><span class="nx">Round</span><span class="err">(</span><span class="nv">$BillableHours</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="err">)</span><span class="w">
            </span><span class="nx">BillablePercentage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">if</span><span class="w"> </span><span class="err">(</span><span class="nv">$TotalHours</span><span class="w"> </span><span class="err">-</span><span class="nx">gt</span><span class="w"> </span><span class="mi">0</span><span class="err">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">[</span><span class="n">Math</span><span class="p">]::</span><span class="n">Round</span><span class="p">((</span><span class="nv">$BillableHours</span><span class="w"> </span><span class="n">/</span><span class="w"> </span><span class="nv">$TotalHours</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nx">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="nx">MonthlyRecurring</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$MonthlyRecurring</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Export report</span><span class="w">
    </span><span class="nv">$ReportPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">".\ClientReport_</span><span class="si">$(</span><span class="nv">$StartDate</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'yyyyMMdd'</span><span class="p">)</span><span class="si">)</span><span class="s2">_to_</span><span class="si">$(</span><span class="nv">$EndDate</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'yyyyMMdd'</span><span class="p">)</span><span class="si">)</span><span class="s2">.csv"</span><span class="w">

    </span><span class="c"># Create summary</span><span class="w">
    </span><span class="nv">$Summary</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">ReportPeriod</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="si">$(</span><span class="nv">$StartDate</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'yyyy-MM-dd'</span><span class="p">)</span><span class="si">)</span><span class="s2"> to </span><span class="si">$(</span><span class="nv">$EndDate</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'yyyy-MM-dd'</span><span class="p">)</span><span class="si">)</span><span class="s2">"</span><span class="w">
        </span><span class="nx">TotalCompanies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Report</span><span class="err">.</span><span class="nx">Count</span><span class="w">
        </span><span class="nx">TotalTickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nv">$Report</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Measure</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="err">-</span><span class="nx">Property</span><span class="w"> </span><span class="nx">TicketCount</span><span class="w"> </span><span class="err">-</span><span class="nx">Sum</span><span class="err">).</span><span class="nx">Sum</span><span class="w">
        </span><span class="nx">TotalHours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nv">$Report</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Measure</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="err">-</span><span class="nx">Property</span><span class="w"> </span><span class="nx">TotalHours</span><span class="w"> </span><span class="err">-</span><span class="nx">Sum</span><span class="err">).</span><span class="nx">Sum</span><span class="w">
        </span><span class="nx">TotalBillableHours</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nv">$Report</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Measure</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="err">-</span><span class="nx">Property</span><span class="w"> </span><span class="nx">BillableHours</span><span class="w"> </span><span class="err">-</span><span class="nx">Sum</span><span class="err">).</span><span class="nx">Sum</span><span class="w">
        </span><span class="nx">TotalMRR</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nv">$Report</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Measure</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="err">-</span><span class="nx">Property</span><span class="w"> </span><span class="nx">MonthlyRecurring</span><span class="w"> </span><span class="err">-</span><span class="nx">Sum</span><span class="err">).</span><span class="nx">Sum</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Display summary</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">Report Summary:"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">
    </span><span class="nv">$Summary</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-List</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">Top 10 Companies by Ticket Volume:"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">
    </span><span class="nv">$Report</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sort-Object</span><span class="w"> </span><span class="nx">TicketCount</span><span class="w"> </span><span class="nt">-Descending</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w">

    </span><span class="c"># Export to CSV</span><span class="w">
    </span><span class="nv">$Report</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$ReportPath</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">Full report exported to: </span><span class="nv">$ReportPath</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">

    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$Report</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Generate report</span><span class="w">
</span><span class="nv">$Report</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-MonthlyClientReport</span><span class="w">
</span></code></pre></div></div>

<h2 id="best-practices-for-advanced-automation">Best Practices for Advanced Automation</h2>

<h3 id="error-handling">Error Handling</h3>

<p>Always implement robust error handling:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$Result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-TicketID</span><span class="w"> </span><span class="nx">12345</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"Failed to retrieve ticket: </span><span class="bp">$_</span><span class="s2">"</span><span class="w">
    </span><span class="c"># Log to your logging system</span><span class="w">
    </span><span class="c"># Send notification</span><span class="w">
    </span><span class="c"># Implement retry logic if appropriate</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="logging">Logging</h3>

<p>Create detailed logs for troubleshooting:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Write-AutomationLog</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Message</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s1">'Info'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Warning'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Error'</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Level</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Info'</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$LogEntry</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"[</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyy-MM-dd HH:mm:ss'</span><span class="p">)</span><span class="err">]</span><span class="w"> </span><span class="p">[</span><span class="nv">$Level</span><span class="p">]</span><span class="w"> </span><span class="nv">$Message</span><span class="s2">"
    Add-Content -Path "</span><span class="n">C:\Logs\CWMAutomation.log</span><span class="s2">" -Value </span><span class="nv">$LogEntry</span><span class="s2">

    switch (</span><span class="nv">$Level</span><span class="s2">) {
        'Warning' { Write-Warning </span><span class="nv">$Message</span><span class="s2"> }
        'Error'   { Write-Error </span><span class="nv">$Message</span><span class="s2"> }
        default   { Write-Host </span><span class="nv">$Message</span><span class="s2"> }
    }
}
</span></code></pre></div></div>

<h3 id="rate-limiting">Rate Limiting</h3>

<p>Be mindful of API rate limits:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-CWMWithRateLimit</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">scriptblock</span><span class="p">]</span><span class="nv">$ScriptBlock</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$DelayMilliseconds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$Result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">$ScriptBlock</span><span class="w">
    </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Milliseconds</span><span class="w"> </span><span class="nv">$DelayMilliseconds</span><span class="w">
    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$Result</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="testing">Testing</h3>

<p>Always test with a small dataset first:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Test with one company before running on all</span><span class="w">
</span><span class="nv">$TestCompany</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-CompanyID</span><span class="w"> </span><span class="nx">123</span><span class="w">
</span><span class="c"># Run your automation on $TestCompany</span><span class="w">
</span><span class="c"># Verify results</span><span class="w">
</span><span class="c"># Then run on all companies</span><span class="w">
</span></code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>These advanced scenarios demonstrate the power of automating ConnectWise Manage with PowerShell. By combining multiple functions, integrating with other systems, and implementing proper error handling, you can build robust automation that saves time and improves accuracy.</p>

<p>Remember to:</p>
<ul>
  <li>Start small and iterate</li>
  <li>Test thoroughly before production use</li>
  <li>Implement logging and monitoring</li>
  <li>Handle errors gracefully</li>
  <li>Document your automations</li>
</ul>

<p>In the next post, we’ll cover best practices, performance optimization, and tips from years of real-world usage.</p>

<hr />

<p><strong>Series Navigation:</strong></p>
<ul>
  <li>Previous: <a href="/2024/12/02/getting-started-connectwisemanageapi.html">Getting Started with ConnectWiseManageAPI</a></li>
  <li>Next: <a href="/2024/12/16/best-practices-connectwisemanageapi.html">Best Practices and Tips</a></li>
</ul>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="PSA" /><category term="ConnectWiseManageAPI" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Manage" /><category term="PSA" /><category term="Automation" /><category term="Advanced" /><category term="Integration" /><category term="MSP" /><summary type="html"><![CDATA[Beyond the basics: real automation examples IT teams use daily. Learn automated ticket enrichment, bulk time entries, data quality monitoring, custom reporting, and system integrations with production-ready code.]]></summary></entry><entry><title type="html">Getting Started with ConnectWiseManageAPI: Your First Steps</title><link href="https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/23/getting-started-connectwisemanageapi.html" rel="alternate" type="text/html" title="Getting Started with ConnectWiseManageAPI: Your First Steps" /><published>2024-09-23T10:00:00+00:00</published><updated>2024-09-23T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/23/getting-started-connectwisemanageapi</id><content type="html" xml:base="https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/23/getting-started-connectwisemanageapi.html"><![CDATA[<p>In this guide, we’ll walk through everything you need to know to start using ConnectWiseManageAPI in your environment. By the end, you’ll have the module installed, configured, and you’ll have executed your first automation tasks.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>Before we begin, you’ll need:</p>

<ol>
  <li><strong>PowerShell 3.0 or higher</strong> (PowerShell 5.1+ recommended)</li>
  <li><strong>A ConnectWise Manage account</strong> with API access</li>
  <li><strong>API credentials</strong> from ConnectWise Manage</li>
  <li><strong>A Client ID</strong> from the ConnectWise Developer Portal</li>
</ol>

<p>Don’t worry—we’ll walk through obtaining all of these.</p>

<h2 id="step-1-obtaining-api-credentials">Step 1: Obtaining API Credentials</h2>

<p>To use the ConnectWise Manage API, you need to generate API keys from your Manage instance.</p>

<h3 id="creating-api-keys">Creating API Keys</h3>

<ol>
  <li>Log into ConnectWise Manage</li>
  <li>Navigate to <strong>System &gt; Members</strong></li>
  <li>Select your member account (or create a dedicated API user)</li>
  <li>Click on <strong>API Keys</strong> tab</li>
  <li>Click <strong>Add</strong> to create a new API key pair</li>
  <li>Enter a description (e.g., “PowerShell Automation”)</li>
  <li>Save the <strong>Public Key</strong> and <strong>Private Key</strong> securely</li>
</ol>

<p><strong>Important Security Notes:</strong></p>
<ul>
  <li>Treat these keys like passwords—never commit them to source control</li>
  <li>Consider creating a dedicated API user account with appropriate permissions</li>
  <li>Store keys in a secure location like Azure Key Vault</li>
  <li>Rotate keys periodically</li>
</ul>

<h3 id="getting-your-client-id">Getting Your Client ID</h3>

<p>As of August 2019, ConnectWise requires a Client ID for all API interactions.</p>

<ol>
  <li>Visit <a href="https://developer.connectwise.com/ClientID">https://developer.connectwise.com/ClientID</a></li>
  <li>Log in with your ConnectWise credentials</li>
  <li>Register your application</li>
  <li>Copy your Client ID</li>
</ol>

<p>This Client ID is unique to your integration and helps ConnectWise track API usage.</p>

<h2 id="step-2-installing-the-module">Step 2: Installing the Module</h2>

<p>Installing ConnectWiseManageAPI is straightforward using PowerShell Gallery.</p>

<h3 id="first-time-installation">First-Time Installation</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="s1">'ConnectWiseManageAPI'</span><span class="w">
</span></code></pre></div></div>

<p>If you encounter issues accessing PowerShell Gallery, you may need to initialize it first. Check out the <a href="https://github.com/christaylorcodes/Initialize-PSGallery">Initialize-PSGallery</a> helper script.</p>

<h3 id="updating-the-module">Updating the Module</h3>

<p>To update to the latest version:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Update-Module</span><span class="w"> </span><span class="s1">'ConnectWiseManageAPI'</span><span class="w">
</span></code></pre></div></div>

<h3 id="verifying-installation">Verifying Installation</h3>

<p>Confirm the module is installed and check its version:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Module</span><span class="w"> </span><span class="s1">'ConnectWiseManageAPI'</span><span class="w"> </span><span class="nt">-ListAvailable</span><span class="w">
</span></code></pre></div></div>

<p>Import the module:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Import-Module</span><span class="w"> </span><span class="s1">'ConnectWiseManageAPI'</span><span class="w">
</span></code></pre></div></div>

<p>View available commands:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Command</span><span class="w"> </span><span class="nt">-Module</span><span class="w"> </span><span class="s1">'ConnectWiseManageAPI'</span><span class="w">
</span></code></pre></div></div>

<p>You should see 180+ cmdlets available for use.</p>

<h2 id="step-3-connecting-to-connectwise-manage">Step 3: Connecting to ConnectWise Manage</h2>

<p>Now that you have credentials and the module installed, let’s connect to your Manage server.</p>

<h3 id="basic-connection">Basic Connection</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ConnectionInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'na.myconnectwise.net'</span><span class="w">  </span><span class="c"># Your Manage server URL</span><span class="w">
    </span><span class="nx">Company</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'YourCompanyID'</span><span class="w">          </span><span class="c"># Your company identifier</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-public-key'</span><span class="w">        </span><span class="c"># Public API key</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-private-key'</span><span class="w">       </span><span class="c"># Private API key</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-client-id'</span><span class="w">         </span><span class="c"># Client ID from developer portal</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">ConnectionInfo</span><span class="w">
</span></code></pre></div></div>

<h3 id="secure-credential-storage">Secure Credential Storage</h3>

<p>For production use, don’t hardcode credentials. Here are better approaches:</p>

<p><strong>Option 1: Using Azure Key Vault</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Retrieve credentials from Azure Key Vault</span><span class="w">
</span><span class="nv">$pubkey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-AzKeyVaultSecret</span><span class="w"> </span><span class="nt">-VaultName</span><span class="w"> </span><span class="s1">'YourVault'</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s1">'CWM-PublicKey'</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w">
</span><span class="nv">$privatekey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-AzKeyVaultSecret</span><span class="w"> </span><span class="nt">-VaultName</span><span class="w"> </span><span class="s1">'YourVault'</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s1">'CWM-PrivateKey'</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w">
</span><span class="nv">$clientid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-AzKeyVaultSecret</span><span class="w"> </span><span class="nt">-VaultName</span><span class="w"> </span><span class="s1">'YourVault'</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s1">'CWM-ClientID'</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w">

</span><span class="nv">$ConnectionInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'na.myconnectwise.net'</span><span class="w">
    </span><span class="nx">Company</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'YourCompanyID'</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nv">$pubkey</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="nv">$privatekey</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$clientid</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">ConnectionInfo</span><span class="w">
</span></code></pre></div></div>

<p><strong>Option 2: Using Environment Variables</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ConnectionInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_SERVER</span><span class="w">
    </span><span class="nx">Company</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_COMPANY</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_PUBKEY</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_PRIVATEKEY</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_CLIENTID</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">ConnectionInfo</span><span class="w">
</span></code></pre></div></div>

<p><strong>Option 3: Using a Separate Configuration File</strong></p>

<p>Create a file <code class="language-plaintext highlighter-rouge">CWMConfig.ps1</code> (add to .gitignore):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># CWMConfig.ps1 (DO NOT COMMIT TO SOURCE CONTROL)</span><span class="w">
</span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'na.myconnectwise.net'</span><span class="w">
    </span><span class="nx">Company</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'YourCompanyID'</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-public-key'</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-private-key'</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-client-id'</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Load it in your scripts:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ConnectionInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="o">.</span><span class="n">\CWMConfig.ps1</span><span class="w">
</span><span class="nx">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">ConnectionInfo</span><span class="w">
</span></code></pre></div></div>

<h3 id="verifying-your-connection">Verifying Your Connection</h3>

<p>After connecting, verify it worked:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMSystemInfo</span><span class="w">
</span></code></pre></div></div>

<p>This should return information about your ConnectWise Manage environment.</p>

<h2 id="step-4-your-first-commands">Step 4: Your First Commands</h2>

<p>Let’s try some basic operations to get familiar with the module.</p>

<h3 id="retrieving-companies">Retrieving Companies</h3>

<p>Get all companies (this may take a moment if you have many):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Companies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span><span class="nv">$Companies</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">city</span><span class="p">,</span><span class="w"> </span><span class="nx">state</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w">
</span></code></pre></div></div>

<p>Get a specific company by ID:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-CompanyID</span><span class="w"> </span><span class="nx">123</span><span class="w">
</span></code></pre></div></div>

<p>Search for companies by condition:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"city='Salt Lake City'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<h3 id="working-with-tickets">Working with Tickets</h3>

<p>Get all open tickets:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$OpenTickets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"status/name='Open'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span><span class="nv">$OpenTickets</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">summary</span><span class="p">,</span><span class="w"> </span><span class="nx">company</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w">
</span></code></pre></div></div>

<p>Get tickets for a specific company:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=123"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<p>Get a single ticket with all details:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Ticket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-TicketID</span><span class="w"> </span><span class="nx">456</span><span class="w">
</span><span class="nv">$Ticket</span><span class="w">
</span></code></pre></div></div>

<h3 id="creating-a-new-ticket">Creating a New Ticket</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$NewTicketParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">summary</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Test ticket from PowerShell'</span><span class="w">
    </span><span class="nx">company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">123</span><span class="p">}</span><span class="w">
    </span><span class="nx">board</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="w">
    </span><span class="nx">contactName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'John Smith'</span><span class="w">
    </span><span class="nx">contactEmailAddress</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'john.smith@example.com'</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$NewTicket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-CWMTicket</span><span class="w"> </span><span class="err">@</span><span class="nx">NewTicketParams</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Created ticket </span><span class="si">$(</span><span class="nv">$NewTicket</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span></code></pre></div></div>

<h3 id="adding-a-note-to-a-ticket">Adding a Note to a Ticket</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$NoteParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">ticketId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$NewTicket</span><span class="err">.</span><span class="nx">id</span><span class="w">
    </span><span class="nx">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'This ticket was created via the API for testing purposes.'</span><span class="w">
    </span><span class="nx">internalAnalysisFlag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">New-CWMTicketNote</span><span class="w"> </span><span class="err">@</span><span class="nx">NoteParams</span><span class="w">
</span></code></pre></div></div>

<h3 id="retrieving-service-boards">Retrieving Service Boards</h3>

<p>Get all service boards:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMServiceBoard</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<p>Get statuses for a specific board:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMServiceBoardStatus</span><span class="w"> </span><span class="nt">-BoardID</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<h3 id="working-with-contacts">Working with Contacts</h3>

<p>Search for contacts:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMContact</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"company/id=123"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<p>Get contact details:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMContact</span><span class="w"> </span><span class="nt">-ContactID</span><span class="w"> </span><span class="nx">789</span><span class="w">
</span></code></pre></div></div>

<h2 id="step-5-understanding-common-parameters">Step 5: Understanding Common Parameters</h2>

<p>Most GET functions in ConnectWiseManageAPI support these parameters:</p>

<h3 id="filtering-with-conditions">Filtering with Conditions</h3>

<p>The <code class="language-plaintext highlighter-rouge">condition</code> parameter uses ConnectWise’s query syntax:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Simple equality</span><span class="w">
</span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"status/name='Open'"</span><span class="w">

</span><span class="c"># Multiple conditions with AND</span><span class="w">
</span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"status/name='Open' and priority/id=1"</span><span class="w">

</span><span class="c"># Using contains</span><span class="w">
</span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"name contains 'Tech'"</span><span class="w">

</span><span class="c"># Comparison operators</span><span class="w">
</span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"id &gt; 1000"</span><span class="w">

</span><span class="c"># Using OR requires parentheses</span><span class="w">
</span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"(status/name='Open' or status/name='New')"</span><span class="w">
</span></code></pre></div></div>

<h3 id="sorting-results">Sorting Results</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-orderBy</span><span class="w"> </span><span class="s2">"id desc"</span><span class="w"> </span><span class="nt">-pageSize</span><span class="w"> </span><span class="nx">10</span><span class="w">
</span></code></pre></div></div>

<h3 id="selecting-specific-fields">Selecting Specific Fields</h3>

<p>Reduce data transfer by requesting only needed fields:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-fields</span><span class="w"> </span><span class="s2">"id,name,city,state"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<h3 id="pagination">Pagination</h3>

<p>For large datasets, use pagination:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get first page (25 records by default)</span><span class="w">
</span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-page</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="nt">-pageSize</span><span class="w"> </span><span class="nx">25</span><span class="w">

</span><span class="c"># Get all results automatically</span><span class="w">
</span><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<h3 id="getting-record-counts">Getting Record Counts</h3>

<p>Just want to know how many records match?</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"status/name='Open'"</span><span class="w"> </span><span class="nt">-count</span><span class="w">
</span></code></pre></div></div>

<h2 id="step-6-a-complete-example">Step 6: A Complete Example</h2>

<p>Let’s put it all together with a practical example: creating a ticket with all the details.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Import the module</span><span class="w">
</span><span class="n">Import-Module</span><span class="w"> </span><span class="s1">'ConnectWiseManageAPI'</span><span class="w">

</span><span class="c"># Connect (using secure method)</span><span class="w">
</span><span class="nv">$ConnectionInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'na.myconnectwise.net'</span><span class="w">
    </span><span class="nx">Company</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'YourCompanyID'</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_PUBKEY</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_PRIVATEKEY</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CWM_CLIENTID</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">ConnectionInfo</span><span class="w"> </span><span class="nt">-Verbose</span><span class="w">

</span><span class="c"># Get company interactively</span><span class="w">
</span><span class="nv">$Companies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWMCompany</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span><span class="nv">$Company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Companies</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Out-GridView</span><span class="w"> </span><span class="nt">-OutputMode</span><span class="w"> </span><span class="nx">Single</span><span class="w"> </span><span class="nt">-Title</span><span class="w"> </span><span class="s1">'Select Company'</span><span class="w">

</span><span class="c"># Create ticket parameters</span><span class="w">
</span><span class="nv">$TicketParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">summary</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Network connectivity issue in Building A'</span><span class="w">
    </span><span class="nx">company</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Company</span><span class="err">.</span><span class="nx">id</span><span class="p">}</span><span class="w">
    </span><span class="nx">board</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="w">
    </span><span class="nx">contactName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Sarah Johnson'</span><span class="w">
    </span><span class="nx">contactPhoneNumber</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'555-0123'</span><span class="w">
    </span><span class="nx">contactEmailAddress</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'sarah.johnson@example.com'</span><span class="w">
    </span><span class="nx">priority</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">3</span><span class="p">}</span><span class="w">  </span><span class="c"># Assuming 3 is Medium priority</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Create the ticket</span><span class="w">
</span><span class="nv">$Ticket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-CWMTicket</span><span class="w"> </span><span class="err">@</span><span class="nx">TicketParams</span><span class="w">

</span><span class="c"># Add initial note</span><span class="w">
</span><span class="nv">$NoteParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">ticketId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Ticket</span><span class="err">.</span><span class="nx">id</span><span class="w">
    </span><span class="nx">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Initial investigation notes:</span><span class="se">`n</span><span class="s2">- Issue reported at 9:30 AM</span><span class="se">`n</span><span class="s2">- Affects 5 workstations</span><span class="se">`n</span><span class="s2">- Network switch shows amber light"</span><span class="w">
    </span><span class="nx">internalAnalysisFlag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">New-CWMTicketNote</span><span class="w"> </span><span class="err">@</span><span class="nx">NoteParams</span><span class="w">

</span><span class="c"># Display result</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Successfully created ticket #</span><span class="si">$(</span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Summary: </span><span class="si">$(</span><span class="nv">$Ticket</span><span class="o">.</span><span class="nf">summary</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Company: </span><span class="si">$(</span><span class="nv">$Company</span><span class="o">.</span><span class="nf">name</span><span class="si">)</span><span class="s2">"</span><span class="w">

</span><span class="c"># Clean up connection</span><span class="w">
</span><span class="n">Disconnect-CWM</span><span class="w">
</span></code></pre></div></div>

<h2 id="getting-help">Getting Help</h2>

<h3 id="built-in-help">Built-in Help</h3>

<p>Every function includes comment-based help:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Help</span><span class="w"> </span><span class="nx">Get-CWMTicket</span><span class="w"> </span><span class="nt">-Full</span><span class="w">
</span><span class="n">Get-Help</span><span class="w"> </span><span class="nx">New-CWMTicket</span><span class="w"> </span><span class="nt">-Examples</span><span class="w">
</span></code></pre></div></div>

<h3 id="additional-resources">Additional Resources</h3>

<ul>
  <li><a href="https://github.com/christaylorcodes/ConnectWiseManageAPI">GitHub Repository</a></li>
  <li><a href="https://www.powershellgallery.com/packages/ConnectWiseManageAPI">PowerShell Gallery</a></li>
  <li><a href="https://developer.connectwise.com/Products/Manage/REST">ConnectWise API Documentation</a></li>
</ul>

<h2 id="troubleshooting-common-issues">Troubleshooting Common Issues</h2>

<h3 id="authentication-failures">Authentication Failures</h3>

<p><strong>Error:</strong> “401 Unauthorized”</p>

<p><strong>Solutions:</strong></p>
<ul>
  <li>Verify your API keys are correct</li>
  <li>Ensure your Client ID is valid</li>
  <li>Check that the API user account is active</li>
  <li>Confirm the company ID matches your login company</li>
</ul>

<h3 id="connection-timeouts">Connection Timeouts</h3>

<p><strong>Error:</strong> “The operation has timed out”</p>

<p><strong>Solutions:</strong></p>
<ul>
  <li>Verify your server URL is correct (include ‘https://’ if needed)</li>
  <li>Check network connectivity to the Manage server</li>
  <li>Ensure firewall rules allow outbound HTTPS</li>
</ul>

<h3 id="rate-limiting">Rate Limiting</h3>

<p><strong>Error:</strong> “429 Too Many Requests”</p>

<p><strong>Solutions:</strong></p>
<ul>
  <li>Implement delays between API calls</li>
  <li>Use the <code class="language-plaintext highlighter-rouge">-all</code> parameter instead of manual pagination loops</li>
  <li>Consider batching operations</li>
</ul>

<h3 id="empty-results">Empty Results</h3>

<p><strong>Problem:</strong> Query returns no results when you expect data</p>

<p><strong>Solutions:</strong></p>
<ul>
  <li>Verify condition syntax (check for typos)</li>
  <li>Test condition in ConnectWise Manage’s API directly</li>
  <li>Check field names are correct (they’re case-sensitive)</li>
  <li>Ensure you have permissions to view the data</li>
</ul>

<h2 id="next-steps">Next Steps</h2>

<p>Now that you’re up and running, you can:</p>

<ol>
  <li><strong>Explore the Examples</strong> - Check out the GitHub repository for more scenarios</li>
  <li><strong>Build Your First Automation</strong> - Identify a repetitive task and automate it</li>
  <li><strong>Create Custom Reports</strong> - Extract data and create reports tailored to your needs</li>
  <li><strong>Integrate Systems</strong> - Connect ConnectWise Manage with your other tools</li>
</ol>

<p>In the next blog post, we’ll explore advanced automation scenarios including building complex integrations, handling bulk operations efficiently, and creating scheduled automation workflows.</p>

<hr />

<p><strong>Series Navigation:</strong></p>
<ul>
  <li>Previous: <a href="/2024/11/25/introducing-connectwisemanageapi.html">Introducing ConnectWiseManageAPI</a></li>
  <li>Next: <a href="/2024/12/09/advanced-automation-connectwisemanageapi.html">Advanced Automation Scenarios</a></li>
</ul>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="PSA" /><category term="ConnectWiseManageAPI" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Manage" /><category term="PSA" /><category term="Tutorial" /><category term="Getting-Started" /><category term="MSP" /><category term="API" /><summary type="html"><![CDATA[Step-by-step guide to start automating ConnectWise Manage. Learn how to get API credentials, install the module, connect to your server, and run your first automation tasks with troubleshooting help.]]></summary></entry><entry><title type="html">Introducing ConnectWiseManageAPI: PowerShell Automation for MSPs</title><link href="https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/16/introducing-connectwisemanageapi.html" rel="alternate" type="text/html" title="Introducing ConnectWiseManageAPI: PowerShell Automation for MSPs" /><published>2024-09-16T10:00:00+00:00</published><updated>2024-09-16T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/16/introducing-connectwisemanageapi</id><content type="html" xml:base="https://christaylor.codes/powershell/psa/connectwisemanageapi/2024/09/16/introducing-connectwisemanageapi.html"><![CDATA[<h2 id="the-challenge-of-manual-psa-management">The Challenge of Manual PSA Management</h2>

<p>If you’re running a Managed Service Provider (MSP) or working in IT operations, you know that ConnectWise Manage is an incredibly powerful Professional Services Automation (PSA) platform. It handles everything from ticketing and time tracking to projects and billing. But with great power comes great responsibility—and often, a lot of repetitive clicking.</p>

<p>As MSPs, we automate everything for our clients: patch management, backups, monitoring, you name it. But when it comes to managing our own business operations in ConnectWise Manage, many of us are still clicking through web interfaces, manually updating tickets, copying data between systems, and performing the same tasks over and over.</p>

<p>There had to be a better way.</p>

<h2 id="enter-powershell-and-the-connectwise-manage-api">Enter PowerShell and the ConnectWise Manage API</h2>

<p>ConnectWise provides a comprehensive REST API that exposes nearly every function of their platform. This is great news for automation enthusiasts. However, working directly with REST APIs can be tedious:</p>

<ul>
  <li>Constructing URLs with proper query parameters</li>
  <li>Managing authentication headers</li>
  <li>Handling pagination for large result sets</li>
  <li>Converting between PowerShell objects and JSON</li>
  <li>Dealing with API quirks and error handling</li>
</ul>

<p>That’s where <strong>ConnectWiseManageAPI</strong> comes in.</p>

<h2 id="what-is-connectwisemanageapi">What is ConnectWiseManageAPI?</h2>

<p>ConnectWiseManageAPI is a PowerShell module that provides a comprehensive wrapper for the ConnectWise Manage REST API. Instead of wrestling with REST calls, you can use familiar PowerShell cmdlets to interact with every aspect of your Manage environment.</p>

<p>The module currently provides <strong>180+ cmdlets</strong> covering all major areas of ConnectWise Manage:</p>

<ul>
  <li><strong>Service Management</strong>: Tickets, service boards, notes, configurations</li>
  <li><strong>Company Management</strong>: Companies, contacts, sites, and relationships</li>
  <li><strong>Time Tracking</strong>: Time entries, timesheets, and approvals</li>
  <li><strong>Project Management</strong>: Projects, phases, team members, and tickets</li>
  <li><strong>Finance</strong>: Agreements, billing, and invoicing</li>
  <li><strong>System Administration</strong>: Members, audit trails, documents, and system info</li>
</ul>

<h2 id="why-powershell">Why PowerShell?</h2>

<p>PowerShell is the natural choice for IT automation, especially in MSP environments. Here’s why:</p>

<p><strong>1. It’s Already in Your Toolkit</strong>
Most MSPs are already using PowerShell for RMM scripts, Active Directory management, and automation tasks. ConnectWiseManageAPI extends that same skillset to your PSA.</p>

<p><strong>2. Pipeline Magic</strong>
PowerShell’s pipeline makes it incredibly easy to chain operations together. Want to get all open tickets for a specific company and export them to CSV? One line of code.</p>

<p><strong>3. Integration with Everything</strong>
PowerShell can interact with virtually any system: Active Directory, Azure, AWS, databases, other APIs. This makes it perfect for building integrations between ConnectWise Manage and your entire technology stack.</p>

<p><strong>4. Scheduled Automation</strong>
Use Windows Task Scheduler to run PowerShell scripts on a schedule. Daily reports? Automated ticket updates? Scheduled maintenance tasks? All possible.</p>

<h2 id="real-world-use-cases">Real-World Use Cases</h2>

<p>Here are just a few ways MSPs are using ConnectWiseManageAPI:</p>

<p><strong>Automated Ticket Creation</strong>
Your monitoring tools detect an issue and automatically create a ticket with all relevant details, assigned to the right team, on the correct board.</p>

<p><strong>Bulk Operations</strong>
Update 500 company records with new contact information. Apply changes to multiple tickets matching specific criteria. Operations that would take hours of clicking are done in seconds.</p>

<p><strong>Custom Reporting</strong>
Extract data from Manage, combine it with information from other sources, and generate custom reports that exactly match your business needs.</p>

<p><strong>Time Entry Automation</strong>
Automatically create time entries based on activities in other systems. No more forgetting to log time.</p>

<p><strong>Integration Workflows</strong>
Connect ConnectWise Manage with your RMM, documentation platform, billing system, and more. Build the integrated MSP stack you’ve always wanted.</p>

<p><strong>Data Validation and Cleanup</strong>
Regularly scan your Manage database for inconsistencies, missing information, or data quality issues, and automatically correct them.</p>

<h2 id="the-master-function-pattern">The Master Function Pattern</h2>

<p>One of the key design principles of ConnectWiseManageAPI is the “Master Function” pattern. Instead of writing unique code for every single API endpoint, the module uses a layered approach:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User Function (Get-CWMTicket)
    ↓
Master Function (Invoke-CWMGetMaster)
    ↓
Web Request Handler (Invoke-CWMWebRequest)
    ↓
ConnectWise Manage REST API
</code></pre></div></div>

<p>This architecture provides several benefits:</p>

<ul>
  <li><strong>Consistency</strong>: All functions behave the same way</li>
  <li><strong>Maintainability</strong>: Bug fixes and improvements benefit all functions</li>
  <li><strong>Reliability</strong>: Centralized error handling and retry logic</li>
  <li><strong>Flexibility</strong>: Easy to add new endpoints as ConnectWise updates their API</li>
</ul>

<h2 id="getting-started-is-easy">Getting Started is Easy</h2>

<p>Installing the module is as simple as any PowerShell module:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="s1">'ConnectWiseManageAPI'</span><span class="w">
</span></code></pre></div></div>

<p>Once installed, you connect to your Manage server with your API credentials:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ConnectionInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Server</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'na.myconnectwise.net'</span><span class="w">
    </span><span class="nx">Company</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'YourCompanyID'</span><span class="w">
    </span><span class="nx">pubkey</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-public-key'</span><span class="w">
    </span><span class="nx">privatekey</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-private-key'</span><span class="w">
    </span><span class="nx">clientid</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-client-id'</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Connect-CWM</span><span class="w"> </span><span class="err">@</span><span class="nx">ConnectionInfo</span><span class="w">
</span></code></pre></div></div>

<p>And you’re ready to start automating. Here’s a simple example that retrieves all open tickets:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-CWMTicket</span><span class="w"> </span><span class="nt">-condition</span><span class="w"> </span><span class="s2">"status/name='Open'"</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span></code></pre></div></div>

<h2 id="open-source-and-community-driven">Open Source and Community-Driven</h2>

<p>ConnectWiseManageAPI is open source and available on <a href="https://github.com/christaylorcodes/ConnectWiseManageAPI">GitHub</a> under the MIT license. The project has grown thanks to contributions from the MSP community:</p>

<ul>
  <li><strong>118 stars</strong> on GitHub</li>
  <li><strong>69 forks</strong> from developers building custom solutions</li>
  <li>Active issue tracking and feature requests</li>
  <li>Regular updates and improvements</li>
</ul>

<p>Whether you’re looking to automate a single task or build a comprehensive integration platform, ConnectWiseManageAPI provides the foundation you need.</p>

<h2 id="whats-next">What’s Next?</h2>

<p>In upcoming blog posts, I’ll dive deeper into:</p>

<ul>
  <li><strong>Getting Started</strong>: A step-by-step guide to setting up and using the module</li>
  <li><strong>Advanced Automation</strong>: Building complex workflows and integrations</li>
  <li><strong>Best Practices</strong>: Tips and patterns from years of real-world MSP usage</li>
</ul>

<p>Stay tuned, and if you want to get started right away, check out the <a href="https://github.com/christaylorcodes/ConnectWiseManageAPI">project on GitHub</a> or install it from the <a href="https://www.powershellgallery.com/packages/ConnectWiseManageAPI">PowerShell Gallery</a>.</p>

<hr />

<p><strong>Resources:</strong></p>
<ul>
  <li><a href="https://github.com/christaylorcodes/ConnectWiseManageAPI">GitHub Repository</a></li>
  <li><a href="https://www.powershellgallery.com/packages/ConnectWiseManageAPI">PowerShell Gallery</a></li>
  <li><a href="https://developer.connectwise.com">ConnectWise Developer Portal</a></li>
</ul>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="PSA" /><category term="ConnectWiseManageAPI" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Manage" /><category term="PSA" /><category term="Automation" /><category term="MSP" /><category term="API" /><summary type="html"><![CDATA[Stop clicking through ConnectWise Manage for every task. This PowerShell module provides 180+ commands to automate tickets, time entries, reports, and integrations. Eliminate repetitive work with simple scripts.]]></summary></entry><entry><title type="html">Practical Use Cases for ConnectWiseAutomateAgent</title><link href="https://christaylor.codes/powershell/automation/2024/03/11/ten-use-cases-connectwise-automate-agent.html" rel="alternate" type="text/html" title="Practical Use Cases for ConnectWiseAutomateAgent" /><published>2024-03-11T10:00:00+00:00</published><updated>2024-03-11T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/automation/2024/03/11/ten-use-cases-connectwise-automate-agent</id><content type="html" xml:base="https://christaylor.codes/powershell/automation/2024/03/11/ten-use-cases-connectwise-automate-agent.html"><![CDATA[<h2 id="what-this-covers">What This Covers</h2>

<p>The ConnectWiseAutomateAgent module handles more than agent installation. It exposes functions for health checks, configuration management, proxy settings, branding, and uninstallation – all scriptable, all composable with standard PowerShell tooling.</p>

<p>This post walks through practical examples: onboarding, compliance reporting, DR testing, version audits, multi-tenant deployment, proxy changes, migrations, network testing, health monitoring, and department branding. Each one is a pattern you can adapt to your environment.</p>

<hr />

<h2 id="new-hire-provisioning">New Hire Provisioning</h2>

<p><strong>Scenario</strong>: Your client onboards new employees weekly. Each workstation needs an RMM (Remote Monitoring and Management) agent installed, configured, and reporting to the correct location.</p>

<p><strong>The manual version</strong>: HR notifies IT, IT provisions the machine, IT manually installs the agent, and the agent eventually shows up in Automate.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Integrated onboarding script</span><span class="w">
</span><span class="kr">param</span><span class="p">(</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$EmployeeName</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Department</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="c"># Determine LocationID based on department</span><span class="w">
</span><span class="nv">$locationMap</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="s1">'Sales'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w">
    </span><span class="s1">'Engineering'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">101</span><span class="w">
    </span><span class="s1">'Support'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">102</span><span class="w">
    </span><span class="s1">'Management'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">103</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$locationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$locationMap</span><span class="p">[</span><span class="nv">$Department</span><span class="p">]</span><span class="w">

</span><span class="c"># Provision new computer (your existing process)</span><span class="w">
</span><span class="nv">$computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-ADComputer</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"WKS-</span><span class="nv">$EmployeeName</span><span class="s2">"</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"OU=</span><span class="nv">$Department</span><span class="s2">,DC=company,DC=com"</span><span class="w"> </span><span class="nt">-PassThru</span><span class="w">

</span><span class="c"># Wait for computer to come online</span><span class="w">
</span><span class="kr">do</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">30</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">until</span><span class="w"> </span><span class="p">(</span><span class="n">Test-Connection</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="nt">-Count</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="nt">-Quiet</span><span class="p">)</span><span class="w">

</span><span class="c"># Install RMM agent</span><span class="w">
</span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Install-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
    </span><span class="n">Install-CWAA</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="s1">'https://automate.msp.com'</span><span class="w"> </span><span class="se">`
</span><span class="w">                 </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">token</span><span class="w"> </span><span class="se">`
</span><span class="w">                 </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">locationID</span><span class="w"> </span><span class="se">`
</span><span class="w">                 </span><span class="nt">-Hide</span><span class="w"> </span><span class="se">`
</span><span class="w">                 </span><span class="nt">-Rename</span><span class="w"> </span><span class="s2">"Company IT Services"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Verify</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nx">30</span><span class="w">
</span><span class="nv">$agentInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Get-CWAAInfo</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Update ticket/notification</span><span class="w">
</span><span class="n">Send-MailMessage</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="s2">"it@company.com"</span><span class="w"> </span><span class="se">`
</span><span class="w">                 </span><span class="nt">-Subject</span><span class="w"> </span><span class="s2">"New Hire Setup Complete: </span><span class="nv">$EmployeeName</span><span class="s2">"</span><span class="w"> </span><span class="se">`
</span><span class="w">                 </span><span class="nt">-Body</span><span class="w"> </span><span class="s2">"Computer: </span><span class="si">$(</span><span class="nv">$computer</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="se">`n</span><span class="s2">Agent ID: </span><span class="si">$(</span><span class="nv">$agentInfo</span><span class="o">.</span><span class="nf">ID</span><span class="si">)</span><span class="se">`n</span><span class="s2">Location: </span><span class="nv">$Department</span><span class="s2">"</span><span class="w">
</span></code></pre></div></div>

<p><strong>Result</strong>: Fully automated provisioning from AD computer creation to RMM monitoring in minutes.</p>

<hr />

<h2 id="compliance-reporting">Compliance Reporting</h2>

<p><strong>Scenario</strong>: Monthly compliance reports require knowing which machines have RMM agents and which don’t.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Monthly compliance check</span><span class="w">
</span><span class="nv">$allComputers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-SearchBase</span><span class="w"> </span><span class="s2">"DC=company,DC=com"</span><span class="w">
</span><span class="nv">$complianceReport</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$allComputers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$agentInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Get-CWAAInfo</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">Stop</span><span class="w">

        </span><span class="nv">$complianceReport</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="err">.</span><span class="nx">Name</span><span class="w">
            </span><span class="nx">HasAgent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
            </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$agentInfo</span><span class="err">.</span><span class="nx">ID</span><span class="w">
            </span><span class="nx">LastContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$agentInfo</span><span class="err">.</span><span class="nx">LastSuccessfulContact</span><span class="w">
            </span><span class="nx">Version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$agentInfo</span><span class="err">.</span><span class="nx">Version</span><span class="w">
            </span><span class="nx">Compliant</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$complianceReport</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="err">.</span><span class="nx">Name</span><span class="w">
            </span><span class="nx">HasAgent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
            </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">LastContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">Version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">Compliant</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Generate report</span><span class="w">
</span><span class="nv">$complianceReport</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"Compliance-</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyy-MM'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">" -NoTypeInformation

# Email non-compliant systems
</span><span class="nv">$nonCompliant</span><span class="s2"> = </span><span class="nv">$complianceReport</span><span class="s2"> | Where-Object { -not </span><span class="bp">$_</span><span class="s2">.Compliant }
if (</span><span class="nv">$nonCompliant</span><span class="s2">) {
    </span><span class="nv">$body</span><span class="s2"> = </span><span class="nv">$nonCompliant</span><span class="s2"> | ConvertTo-Html | Out-String
    Send-MailMessage -To "</span><span class="n">compliance</span><span class="err">@</span><span class="nx">company.com</span><span class="s2">" </span><span class="se">`</span><span class="s2">
                     -Subject "</span><span class="nx">RMM</span><span class="w"> </span><span class="nx">Compliance</span><span class="w"> </span><span class="nx">Alert:</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nv">$nonCompliant</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> systems need attention"</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-Body</span><span class="w"> </span><span class="nv">$body</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-BodyAsHtml</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Result</strong>: Automated monthly compliance reporting with alerts for non-compliant systems.</p>

<hr />

<h2 id="disaster-recovery-testing">Disaster Recovery Testing</h2>

<p><strong>Scenario</strong>: Quarterly DR tests require removing and reinstalling agents to verify you can recover from a complete loss.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># DR Test: Backup, remove, and restore agent configurations</span><span class="w">
</span><span class="kr">param</span><span class="p">([</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$TestComputers</span><span class="p">)</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$TestComputers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Testing DR for </span><span class="nv">$computer</span><span class="s2">..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">

    </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Backup current configuration</span><span class="w">
        </span><span class="nv">$backup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-CWAABackup</span><span class="w">
        </span><span class="nv">$backup</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Clixml</span><span class="w"> </span><span class="s2">"C:\Temp\AgentBackup-</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd'</span><span class="p">)</span><span class="o">.</span><span class="nf">xml</span><span class="s2">"

        # Simulate disaster - remove agent
        Uninstall-CWAA -Force

        # Wait
        Start-Sleep 10

        # Restore from backup
        </span><span class="nv">$restoredSettings</span><span class="s2"> = Import-Clixml "</span><span class="n">C:\Temp\AgentBackup-</span><span class="err">$</span><span class="p">(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd'</span><span class="p">)</span><span class="o">.</span><span class="nf">xml</span><span class="s2">"

        Install-CWAA -Server </span><span class="nv">$restoredSettings</span><span class="s2">.Server </span><span class="se">`</span><span class="s2">
                     -LocationID </span><span class="nv">$restoredSettings</span><span class="s2">.LocationID </span><span class="se">`</span><span class="s2">
                     -InstallerToken </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">token</span><span class="s2">

        # Verify
        </span><span class="nv">$newInfo</span><span class="s2"> = Get-CWAAInfo
        if (</span><span class="nv">$newInfo</span><span class="s2">.ID -and </span><span class="nv">$newInfo</span><span class="s2">.LocationID -eq </span><span class="nv">$restoredSettings</span><span class="s2">.LocationID) {
            Write-Host "</span><span class="n">DR</span><span class="w"> </span><span class="nx">Test</span><span class="w"> </span><span class="nx">PASSED</span><span class="w"> </span><span class="nx">for</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="s2">" -ForegroundColor Green
        }
        else {
            Write-Host "</span><span class="nx">DR</span><span class="w"> </span><span class="nx">Test</span><span class="w"> </span><span class="nx">FAILED</span><span class="w"> </span><span class="nx">for</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="s2">" -ForegroundColor Red
        }
    }
}
</span></code></pre></div></div>

<p><strong>Result</strong>: Automated DR testing with verification that agents can be restored to correct configurations.</p>

<hr />

<h2 id="agent-version-audit-and-update">Agent Version Audit and Update</h2>

<p><strong>Scenario</strong>: A vulnerability is found in agent version 11.1.2345. You need to find and update all affected agents.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Find and update vulnerable agents</span><span class="w">
</span><span class="nv">$vulnerableVersion</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'11.1.2345'</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-ExpandProperty</span><span class="w"> </span><span class="nx">Name</span><span class="w">

</span><span class="nv">$affectedSystems</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$info</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Get-CWAAInfo</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">Stop</span><span class="w">

        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$info</span><span class="o">.</span><span class="nf">Version</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">vulnerableVersion</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="w">
                </span><span class="nx">CurrentVersion</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">Version</span><span class="w">
                </span><span class="nx">NeedsUpdate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Skip offline/inaccessible machines</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="mi">20</span><span class="w">

</span><span class="c"># Report affected systems</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Found </span><span class="si">$(</span><span class="nv">$affectedSystems</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> systems with vulnerable version"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
</span><span class="nv">$affectedSystems</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"VulnerableAgents.csv"</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">

</span><span class="c"># Update all affected systems</span><span class="w">
</span><span class="nv">$affectedSystems</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
        </span><span class="n">Update-CWAA</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="mi">10</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Updates deployed to </span><span class="si">$(</span><span class="nv">$affectedSystems</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> systems"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span></code></pre></div></div>

<p><strong>Result</strong>: Rapid identification and patching of vulnerable agents across entire infrastructure.</p>

<hr />

<h2 id="multi-tenant-client-segregation">Multi-Tenant Client Segregation</h2>

<p><strong>Scenario</strong>: You’re an MSP managing 50+ clients. Each client’s agents need to land in the correct location with the right display name.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Client configuration database</span><span class="w">
</span><span class="nv">$clientConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
    </span><span class="p">@{</span><span class="w"> </span><span class="nx">ClientName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Acme Corp'</span><span class="p">;</span><span class="w"> </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span><span class="w"> </span><span class="nx">Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://automate.msp.com'</span><span class="p">;</span><span class="w"> </span><span class="nx">Token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'token1'</span><span class="p">;</span><span class="w"> </span><span class="nx">DisplayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Acme IT Services'</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">@{</span><span class="w"> </span><span class="nx">ClientName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Globex Inc'</span><span class="p">;</span><span class="w"> </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">200</span><span class="p">;</span><span class="w"> </span><span class="nx">Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://automate.msp.com'</span><span class="p">;</span><span class="w"> </span><span class="nx">Token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'token2'</span><span class="p">;</span><span class="w"> </span><span class="nx">DisplayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Globex Monitoring'</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">@{</span><span class="w"> </span><span class="nx">ClientName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Initech LLC'</span><span class="p">;</span><span class="w"> </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">300</span><span class="p">;</span><span class="w"> </span><span class="nx">Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://automate.msp.com'</span><span class="p">;</span><span class="w"> </span><span class="nx">Token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'token3'</span><span class="p">;</span><span class="w"> </span><span class="nx">DisplayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Initech IT Support'</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="c"># Deploy based on client</span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">Deploy-ClientAgent</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ComputerName</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientName</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$config</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$clientConfig</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">ClientName</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$ClientName</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$config</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"Client </span><span class="nv">$ClientName</span><span class="s2"> not found in configuration"</span><span class="w">
        </span><span class="kr">return</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$ComputerName</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Install-CWAA</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">config</span><span class="o">.</span><span class="nf">Server</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">config</span><span class="o">.</span><span class="nf">Token</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">config</span><span class="o">.</span><span class="nf">LocationID</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-Rename</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">config</span><span class="o">.</span><span class="nf">DisplayName</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-Hide</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Deployed agent for </span><span class="nv">$ClientName</span><span class="s2"> to </span><span class="nv">$ComputerName</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Usage</span><span class="w">
</span><span class="n">Deploy-ClientAgent</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="s2">"ACME-WKS001"</span><span class="w"> </span><span class="nt">-ClientName</span><span class="w"> </span><span class="s2">"Acme Corp"</span><span class="w">
</span></code></pre></div></div>

<p><strong>Result</strong>: Standardized, error-free agent deployment with proper client segregation and branding.</p>

<hr />

<h2 id="proxy-configuration-management">Proxy Configuration Management</h2>

<p><strong>Scenario</strong>: A client changes their proxy server. You need to update all 300 agents.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Update proxy on all agents</span><span class="w">
</span><span class="kr">param</span><span class="p">(</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$NewProxyURL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://newproxy.client.com:8080"</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientLocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="c"># Get all computers for this client</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"https://automate.msp.com/api/computers?locationId=</span><span class="nv">$ClientLocationID</span><span class="s2">"</span><span class="w">

</span><span class="c"># Update proxy on each</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">ComputerName</span><span class="w">

    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
            </span><span class="n">Set-CWAAProxy</span><span class="w"> </span><span class="nt">-ProxyServerURL</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">NewProxyURL</span><span class="w">
            </span><span class="n">Restart-CWAA</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Updated'</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Failed: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="mi">25</span><span class="w">

</span><span class="c"># Report</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"ProxyUpdate-Results.csv"</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Updated proxy on </span><span class="si">$(</span><span class="p">(</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Updated'</span><span class="p">)</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> computers"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span></code></pre></div></div>

<p><strong>Result</strong>: Instant proxy configuration change across entire client infrastructure.</p>

<hr />

<h2 id="pre-migration-agent-removal">Pre-Migration Agent Removal</h2>

<p><strong>Scenario</strong>: You’re migrating a client to a different RMM. You need clean agent removal from 400 endpoints, with verification that nothing is left behind.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Mass agent removal with verification</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-SearchBase</span><span class="w"> </span><span class="s2">"OU=Client,DC=domain,DC=com"</span><span class="w">

</span><span class="nv">$removalResults</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w">

    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
            </span><span class="n">Uninstall-CWAA</span><span class="w"> </span><span class="nt">-Force</span><span class="w">

            </span><span class="c"># Verify removal</span><span class="w">
            </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nx">10</span><span class="w">
            </span><span class="nv">$service</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$service</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Service still present after uninstall"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="c"># Verify registry cleaned</span><span class="w">
            </span><span class="nv">$reg</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="s2">"HKLM:\SOFTWARE\LabTech\Service"</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$reg</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Registry keys still present"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Removed'</span><span class="w">
            </span><span class="nx">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Agent fully removed and verified'</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Failed'</span><span class="w">
            </span><span class="nx">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">Exception</span><span class="err">.</span><span class="nx">Message</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="mi">15</span><span class="w">

</span><span class="c"># Report</span><span class="w">
</span><span class="nv">$removalResults</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"AgentRemoval-</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">" -NoTypeInformation
</span><span class="nv">$successful</span><span class="s2"> = (</span><span class="nv">$removalResults</span><span class="s2"> | Where-Object Status -eq 'Removed').Count
Write-Host "</span><span class="n">Successfully</span><span class="w"> </span><span class="nx">removed</span><span class="w"> </span><span class="nx">agents</span><span class="w"> </span><span class="nx">from</span><span class="w"> </span><span class="nv">$successful</span><span class="w"> </span><span class="nx">/</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nv">$computers</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> computers"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span></code></pre></div></div>

<p><strong>Result</strong>: Clean, verified removal of agents from entire client infrastructure.</p>

<hr />

<h2 id="network-segmentation-testing">Network Segmentation Testing</h2>

<p><strong>Scenario</strong>: You’re testing firewall rules for a new office and need to verify RMM connectivity from the test subnet before deploying agents.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Network connectivity verification script</span><span class="w">
</span><span class="kr">param</span><span class="p">(</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$TestComputers</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://automate.msp.com"</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="nv">$testResults</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$TestComputers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">

        </span><span class="c"># Test all required ports (output is string-based, not objects)</span><span class="w">
        </span><span class="nv">$portOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-CWAAPort</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">Server</span><span class="w">
        </span><span class="nv">$failedPorts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$portOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s1">'Connection failed'</span><span class="w"> </span><span class="p">}</span><span class="w">

        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="w">
            </span><span class="nx">Subnet</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nx">Get</span><span class="err">-</span><span class="nx">NetIPAddress</span><span class="w"> </span><span class="err">-</span><span class="nx">AddressFamily</span><span class="w"> </span><span class="nx">IPv4</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Where</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">InterfaceAlias</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s1">'Loopback'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Select</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="err">-</span><span class="nx">First</span><span class="w"> </span><span class="mi">1</span><span class="err">).</span><span class="nx">IPAddress</span><span class="w">
            </span><span class="nx">PortTestOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nv">$portOutput</span><span class="w"> </span><span class="err">-</span><span class="nx">join</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">"</span><span class="err">)</span><span class="w">
            </span><span class="nx">AllPortsOpen</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nv">$failedPorts</span><span class="err">.</span><span class="nx">Count</span><span class="w"> </span><span class="err">-</span><span class="nx">eq</span><span class="w"> </span><span class="mi">0</span><span class="err">)</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Report</span><span class="w">
</span><span class="nv">$testResults</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w">
</span><span class="nv">$failures</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$testResults</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="o">-not</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">AllPortsOpen</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$failures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">Firewall issues detected on </span><span class="si">$(</span><span class="nv">$failures</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> computers:"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
    </span><span class="nv">$failures</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nx">Computer</span><span class="p">,</span><span class="w"> </span><span class="nx">Subnet</span><span class="p">,</span><span class="w"> </span><span class="nx">PortTestOutput</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"All network connectivity tests passed!"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Result</strong>: Automated firewall rule verification before agent deployment.</p>

<hr />

<h2 id="fleet-health-monitoring">Fleet Health Monitoring</h2>

<p><strong>Scenario</strong>: You want a daily sweep of all agents that catches issues and attempts basic remediation, with a report of what it found.</p>

<p>If you’re looking for per-endpoint self-healing that runs as a scheduled task on each machine, see <a href="/powershell/rmm/2024/03/01/self-healing-connectwise-automate-agents.html">Self-Healing Agents</a>. That post covers <code class="language-plaintext highlighter-rouge">Repair-CWAA</code>, escalation logic, and the health check task in detail.</p>

<p>This pattern is different – it’s a centralized sweep you run from a management server against your whole fleet.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Daily agent health check with auto-remediation</span><span class="w">
</span><span class="c"># Run as scheduled task at 6 AM daily</span><span class="w">

</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-ExpandProperty</span><span class="w"> </span><span class="nx">Name</span><span class="w">
</span><span class="nv">$issues</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$computers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$health</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">

            </span><span class="nv">$info</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAInfo</span><span class="w">
            </span><span class="nv">$services</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="p">,</span><span class="w"> </span><span class="nx">LTSvcMon</span><span class="w">

            </span><span class="c"># Check for issues</span><span class="w">
            </span><span class="nv">$problems</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

            </span><span class="c"># Issue 1: Services stopped</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$services</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-contains</span><span class="w"> </span><span class="s1">'Stopped'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Restart-CWAA</span><span class="w">
                </span><span class="nv">$problems</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s2">"Services were stopped - restarted"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="c"># Issue 2: Old last contact (&gt; 1 hour)</span><span class="w">
            </span><span class="nv">$lastContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">datetime</span><span class="p">]</span><span class="nv">$info</span><span class="o">.</span><span class="nf">LastSuccessfulContact</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">((</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$lastContact</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="p">[</span><span class="n">timespan</span><span class="p">]::</span><span class="n">FromHours</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Invoke-CWAACommand</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"Send Status"</span><span class="w">
                </span><span class="nv">$problems</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s2">"Stale last contact - forced status update"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="c"># Issue 3: Critical errors in logs</span><span class="w">
            </span><span class="nv">$errors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Last</span><span class="w"> </span><span class="nx">100</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s2">"CRITICAL|FATAL"</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$errors</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$problems</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s2">"Critical errors in log: </span><span class="si">$(</span><span class="nv">$errors</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> entries"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$problems</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                    </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="w">
                    </span><span class="nx">Issues</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$problems</span><span class="w"> </span><span class="err">-</span><span class="nx">join</span><span class="w"> </span><span class="s1">'; '</span><span class="w">
                    </span><span class="nx">Remediated</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$health</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$issues</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">$health</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$issues</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">Issues</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Unable to connect: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w">
            </span><span class="nx">Remediated</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Send daily report</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$issues</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$htmlReport</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$issues</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Html</span><span class="w"> </span><span class="nt">-Title</span><span class="w"> </span><span class="s2">"Daily Agent Health Report"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-String</span><span class="w">

    </span><span class="n">Send-MailMessage</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="s2">"ops@msp.com"</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-From</span><span class="w"> </span><span class="s2">"monitoring@msp.com"</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-Subject</span><span class="w"> </span><span class="s2">"Daily Agent Health Report: </span><span class="si">$(</span><span class="nv">$issues</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> issues found"</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-Body</span><span class="w"> </span><span class="nv">$htmlReport</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-BodyAsHtml</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-SmtpServer</span><span class="w"> </span><span class="s2">"smtp.msp.com"</span><span class="w">

    </span><span class="nv">$issues</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"HealthReport-</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">" -NoTypeInformation
}
</span></code></pre></div></div>

<p><strong>Result</strong>: Proactive monitoring and auto-remediation of common agent issues before they become tickets.</p>

<hr />

<h2 id="department-based-agent-branding">Department-Based Agent Branding</h2>

<p><strong>Scenario</strong>: A large enterprise wants different agent display names per department for charge-back reporting.</p>

<p><strong>Scripted version</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Department-based agent naming</span><span class="w">
</span><span class="nv">$departmentMap</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="s1">'OU=Sales'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Sales Department IT'</span><span class="w">
    </span><span class="s1">'OU=Engineering'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Engineering Services'</span><span class="w">
    </span><span class="s1">'OU=HR'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Human Resources IT'</span><span class="w">
    </span><span class="s1">'OU=Finance'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Finance Department IT'</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-Properties</span><span class="w"> </span><span class="nx">DistinguishedName</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$computers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c"># Determine department from DN</span><span class="w">
    </span><span class="nv">$department</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$departmentMap</span><span class="o">.</span><span class="nf">Keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$computer</span><span class="o">.</span><span class="nf">DistinguishedName</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w">
    </span><span class="nv">$displayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$departmentMap</span><span class="p">[</span><span class="nv">$department</span><span class="p">]</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$displayName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
            </span><span class="n">Rename-CWAAAddRemove</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">displayName</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Updated </span><span class="si">$(</span><span class="nv">$computer</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2"> to '</span><span class="nv">$displayName</span><span class="s2">'"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Result</strong>: Automated agent branding based on organizational structure for accurate charge-back.</p>

<hr />

<h2 id="patterns-worth-noting">Patterns Worth Noting</h2>

<p>A few things show up across most of these examples:</p>

<ul>
  <li><strong>Parallel execution</strong> – <code class="language-plaintext highlighter-rouge">ForEach-Object -Parallel</code> keeps things fast when you’re hitting hundreds of machines</li>
  <li><strong>Error handling</strong> – every script accounts for machines being offline or unreachable</li>
  <li><strong>Reporting</strong> – CSV exports and email notifications so you have a record of what happened</li>
  <li><strong>Verification</strong> – confirm the action worked, don’t assume it did</li>
  <li><strong>Composability</strong> – module functions combine naturally with AD, email, and your existing tooling</li>
</ul>

<p>Here’s a starter template if you want to build your own:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Template for custom use cases</span><span class="w">
</span><span class="kr">param</span><span class="p">([</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$Computers</span><span class="p">)</span><span class="w">

</span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">

            </span><span class="c"># Your logic here</span><span class="w">
            </span><span class="c"># - Get-CWAAInfo</span><span class="w">
            </span><span class="c"># - Set-CWAAProxy</span><span class="w">
            </span><span class="c"># - Restart-CWAA</span><span class="w">
            </span><span class="c"># - etc.</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Success</span><span class="w">
        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Success'</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Failure</span><span class="w">
        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Failed'</span><span class="w">
            </span><span class="nx">Error</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">Exception</span><span class="err">.</span><span class="nx">Message</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="mi">20</span><span class="w">

</span><span class="c"># Report results</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"Results.csv"</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span></code></pre></div></div>

<hr />

<p><strong>Getting started:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
</span></code></pre></div></div>

<p>Full function reference and examples: <a href="https://github.com/christaylorcodes/ConnectWiseAutomateAgent">GitHub Repository</a></p>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="Automation" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Automate" /><category term="RMM" /><category term="Use Cases" /><category term="Automation" /><category term="MSP" /><summary type="html"><![CDATA[Practical PowerShell examples for ConnectWiseAutomateAgent beyond basic installation. Covers new hire provisioning, compliance reporting, disaster recovery, version audits, multi-tenant deployment, proxy management, migrations, and department branding.]]></summary></entry><entry><title type="html">Troubleshooting ConnectWise Automate Agents with PowerShell</title><link href="https://christaylor.codes/powershell/troubleshooting/2024/03/04/troubleshooting-connectwise-automate-agents-powershell.html" rel="alternate" type="text/html" title="Troubleshooting ConnectWise Automate Agents with PowerShell" /><published>2024-03-04T10:00:00+00:00</published><updated>2024-03-04T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/troubleshooting/2024/03/04/troubleshooting-connectwise-automate-agents-powershell</id><content type="html" xml:base="https://christaylor.codes/powershell/troubleshooting/2024/03/04/troubleshooting-connectwise-automate-agents-powershell.html"><![CDATA[<h2 id="what-this-covers">What This Covers</h2>

<p>The ConnectWiseAutomateAgent module includes a set of diagnostic and remediation functions for troubleshooting ConnectWise Automate (CWA) agents. This post walks through the common scenarios – agent won’t come online, connectivity issues, services stopping, log analysis – and the PowerShell commands that address each one.</p>

<p>These are structured workflows, not emergency procedures. The goal is a repeatable troubleshooting toolkit you can reach for whenever an agent needs attention.</p>

<hr />

<h2 id="common-scenarios-and-solutions">Common Scenarios and Solutions</h2>

<h3 id="scenario-1-agent-wont-come-online">Scenario 1: Agent Won’t Come Online</h3>

<p><strong>Symptoms:</strong> Agent is installed but not reporting to the Automate console.</p>

<p>The traditional approach involves an RDP session, manually checking services, navigating to <code class="language-plaintext highlighter-rouge">C:\Windows\LTSVC</code>, opening <code class="language-plaintext highlighter-rouge">LTErrors.txt</code> in Notepad, and scrolling through thousands of lines looking for something relevant.</p>

<p>The module approach runs the same checks from your workstation in one block:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Quick diagnostic - all from your workstation</span><span class="w">
</span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nx">PROBLEM-PC</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c"># Check service status</span><span class="w">
    </span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="p">,</span><span class="w"> </span><span class="nx">LTSvcMon</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">Status</span><span class="w">

    </span><span class="c"># Get agent configuration</span><span class="w">
    </span><span class="n">Get-CWAAInfo</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-List</span><span class="w">

    </span><span class="c"># Check recent errors</span><span class="w">
    </span><span class="n">Get-CWAAError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Last</span><span class="w"> </span><span class="nx">50</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s2">"ERROR|CRITICAL"</span><span class="w"> </span><span class="p">}</span><span class="w">

    </span><span class="c"># Test connectivity to server</span><span class="w">
    </span><span class="n">Test-CWAAPort</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="s2">"https://automate.yourmsp.com"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>That covers services, configuration, recent errors, and port connectivity in a single remote call.</p>

<h3 id="scenario-2-intermittent-connectivity">Scenario 2: Intermittent Connectivity</h3>

<p><strong>Symptoms:</strong> Agent keeps going offline and coming back on its own.</p>

<p>Start with a port check, then look at proxy configuration if the standard ports fail:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Comprehensive connectivity check</span><span class="w">
</span><span class="nv">$server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://automate.yourmsp.com"</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-CWAAPort</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$server</span><span class="w">

</span><span class="c"># Check all required ports</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w">

</span><span class="c"># If port 70, 80, or 443 fails, check proxy</span><span class="w">
</span><span class="nv">$proxySettings</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAProxy</span><span class="w">
</span><span class="nx">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$proxySettings</span><span class="o">.</span><span class="nf">ProxyServerURL</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Proxy configured: </span><span class="si">$(</span><span class="nv">$proxySettings</span><span class="o">.</span><span class="nf">ProxyServerURL</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">

    </span><span class="c"># Test if proxy is the issue</span><span class="w">
    </span><span class="n">Set-CWAAProxy</span><span class="w"> </span><span class="nt">-ResetProxy</span><span class="w">
    </span><span class="n">Restart-CWAA</span><span class="w">
    </span><span class="nx">Start-Sleep</span><span class="w"> </span><span class="nx">30</span><span class="w">

    </span><span class="c"># Retest</span><span class="w">
    </span><span class="n">Test-CWAAPort</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$server</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="scenario-3-agent-services-keep-stopping">Scenario 3: Agent Services Keep Stopping</h3>

<p><strong>Symptoms:</strong> LTService or LTSvcMon stops unexpectedly.</p>

<p>Start with the lightest fix and escalate only if needed:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Restart services</span><span class="w">
</span><span class="n">Restart-CWAA</span><span class="w">

</span><span class="c"># Check if services stayed running</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nx">10</span><span class="w">
</span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="p">,</span><span class="w"> </span><span class="nx">LTSvcMon</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">Status</span><span class="p">,</span><span class="w"> </span><span class="nx">StartType</span><span class="w">

</span><span class="c"># If still failing, check the probe errors</span><span class="w">
</span><span class="n">Get-CWAAProbeError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Last</span><span class="w"> </span><span class="nx">100</span><span class="w">

</span><span class="c"># Nuclear option: reinstall</span><span class="w">
</span><span class="nv">$info</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAInfo</span><span class="w">
</span><span class="nx">Redo-CWAA</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$info</span><span class="o">.</span><span class="nf">Server</span><span class="w"> </span><span class="se">`
</span><span class="w">          </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s2">"your-token"</span><span class="w"> </span><span class="se">`
</span><span class="w">          </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nv">$info</span><span class="o">.</span><span class="nf">LocationID</span><span class="w">
</span></code></pre></div></div>

<h3 id="scenario-4-checking-multiple-machines">Scenario 4: Checking Multiple Machines</h3>

<p>If you need to assess agent health across a fleet, the module’s <code class="language-plaintext highlighter-rouge">Test-CWAAHealth</code> function handles this per-endpoint. For a full walkthrough of bulk health monitoring with automated remediation, see the <a href="/powershell/rmm/2024/03/01/self-healing-connectwise-automate-agents.html">Self-Healing Agents</a> post – it covers scheduled health checks, escalation logic, and event log integration.</p>

<p>For a quick ad-hoc sweep across Active Directory (AD), a script like this pulls basic status from each machine:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get all computers from Active Directory</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-SearchBase</span><span class="w"> </span><span class="s2">"OU=Workstations,DC=domain,DC=com"</span><span class="w">

</span><span class="c"># Check agent health on all of them</span><span class="w">
</span><span class="nv">$healthReport</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$info</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAInfo</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w">
            </span><span class="nv">$services</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="p">,</span><span class="w"> </span><span class="nx">LTSvcMon</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">

            </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="w">
                </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">ID</span><span class="w">
                </span><span class="nx">Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">Server</span><span class="w">
                </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">LocationID</span><span class="w">
                </span><span class="nx">LastSuccessfulContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">LastSuccessfulContact</span><span class="w">
                </span><span class="nx">LTServiceStatus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nv">$services</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Where</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="nx">Name</span><span class="w"> </span><span class="err">-</span><span class="nx">eq</span><span class="w"> </span><span class="s1">'LTService'</span><span class="err">).</span><span class="nx">Status</span><span class="w">
                </span><span class="nx">LTSvcMonStatus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nv">$services</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Where</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="nx">Name</span><span class="w"> </span><span class="err">-</span><span class="nx">eq</span><span class="w"> </span><span class="s1">'LTSvcMon'</span><span class="err">).</span><span class="nx">Status</span><span class="w">
                </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Healthy'</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">Stop</span><span class="w">
        </span><span class="nv">$result</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">LastSuccessfulContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">LTServiceStatus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Unknown'</span><span class="w">
            </span><span class="nx">LTSvcMonStatus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Unknown'</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Error: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="mi">20</span><span class="w">

</span><span class="c"># Show results</span><span class="w">
</span><span class="nv">$healthReport</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-GridView</span><span class="w"> </span><span class="nt">-Title</span><span class="w"> </span><span class="s2">"Agent Health Report"</span><span class="w">

</span><span class="c"># Export for analysis</span><span class="w">
</span><span class="nv">$healthReport</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"AgentHealthReport-</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd-HHmmss'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">" -NoTypeInformation
</span></code></pre></div></div>

<p>This is a point-in-time snapshot. For ongoing monitoring, the scheduled health check system in <code class="language-plaintext highlighter-rouge">Register-CWAAHealthCheckTask</code> is a better fit.</p>

<hr />

<h2 id="understanding-agent-logs">Understanding Agent Logs</h2>

<h3 id="lterrorstxt">LTErrors.txt</h3>

<p>The <code class="language-plaintext highlighter-rouge">Get-CWAAError</code> function reads the agent’s error log so you don’t have to open the file manually. A few useful patterns:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get recent errors with context</span><span class="w">
</span><span class="n">Get-CWAAError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Last</span><span class="w"> </span><span class="nx">100</span><span class="w">

</span><span class="c"># Filter for specific issues</span><span class="w">
</span><span class="n">Get-CWAAError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s2">"heartbeat|timeout|connection"</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="c"># Find patterns</span><span class="w">
</span><span class="n">Get-CWAAError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Group-Object</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sort-Object</span><span class="w"> </span><span class="nx">Count</span><span class="w"> </span><span class="nt">-Descending</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">10</span><span class="w">
</span></code></pre></div></div>

<h3 id="probe-errors">Probe Errors</h3>

<p>Probe errors come from the agent’s internal health checks. Same idea – pull them and look for patterns:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Probe errors indicate agent health checks</span><span class="w">
</span><span class="n">Get-CWAAProbeError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Last</span><span class="w"> </span><span class="nx">50</span><span class="w">

</span><span class="c"># Look for recurring patterns</span><span class="w">
</span><span class="n">Get-CWAAProbeError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="s2">"FAIL|ERROR"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Group-Object</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="advanced-techniques">Advanced Techniques</h2>

<h3 id="adjusting-log-levels">Adjusting Log Levels</h3>

<p>When the default logs don’t show enough detail, temporarily increase verbosity:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Increase logging verbosity</span><span class="w">
</span><span class="n">Set-CWAALogLevel</span><span class="w"> </span><span class="nt">-Level</span><span class="w"> </span><span class="nx">Verbose</span><span class="w">

</span><span class="c"># Reproduce the issue (wait for it to occur)</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nx">300</span><span class="w">

</span><span class="c"># Check detailed logs</span><span class="w">
</span><span class="n">Get-CWAAError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Last</span><span class="w"> </span><span class="nx">200</span><span class="w">

</span><span class="c"># Reset to normal logging</span><span class="w">
</span><span class="n">Set-CWAALogLevel</span><span class="w"> </span><span class="nt">-Level</span><span class="w"> </span><span class="nx">Normal</span><span class="w">
</span></code></pre></div></div>

<p>Don’t forget to set it back. Verbose logging on hundreds of endpoints generates a lot of disk I/O.</p>

<h3 id="forcing-agent-commands">Forcing Agent Commands</h3>

<p>You can tell the agent to perform specific actions immediately rather than waiting for its next scheduled cycle:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Force agent to send inventory</span><span class="w">
</span><span class="n">Invoke-CWAACommand</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"Send Inventory"</span><span class="w">

</span><span class="c"># Force status update</span><span class="w">
</span><span class="n">Invoke-CWAACommand</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"Send Status"</span><span class="w">

</span><span class="c"># Update schedule from server</span><span class="w">
</span><span class="n">Invoke-CWAACommand</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"Update Schedule"</span><span class="w">

</span><span class="c"># Available commands:</span><span class="w">
</span><span class="c"># - Send Inventory</span><span class="w">
</span><span class="c"># - Send Status</span><span class="w">
</span><span class="c"># - Update Schedule</span><span class="w">
</span><span class="c"># - Agent Update</span><span class="w">
</span><span class="c"># - And 14 more...</span><span class="w">
</span></code></pre></div></div>

<h3 id="comparing-settings">Comparing Settings</h3>

<p>If an agent was working before and isn’t now, compare current settings against the backup:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get current settings</span><span class="w">
</span><span class="nv">$current</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAInfo</span><span class="w">

</span><span class="c"># Get backed up settings (from before issues started)</span><span class="w">
</span><span class="nv">$backup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAInfoBackup</span><span class="w">

</span><span class="c"># Compare</span><span class="w">
</span><span class="n">Compare-Object</span><span class="w"> </span><span class="nv">$current</span><span class="o">.</span><span class="nf">PSObject</span><span class="o">.</span><span class="nf">Properties</span><span class="w"> </span><span class="nv">$backup</span><span class="o">.</span><span class="nf">PSObject</span><span class="o">.</span><span class="nf">Properties</span><span class="w"> </span><span class="nt">-Property</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">Value</span><span class="w">
</span></code></pre></div></div>

<p>The module creates backups automatically during certain operations. <code class="language-plaintext highlighter-rouge">Get-CWAAInfoBackup</code> reads from the most recent one.</p>

<h3 id="security-decoding">Security Decoding</h3>

<p>For cases where you need to inspect encrypted agent values:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Decode server password or other encrypted values</span><span class="w">
</span><span class="nv">$encodedValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Base64EncodedValueFromRegistry"</span><span class="w">
</span><span class="nv">$decoded</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ConvertFrom-CWAASecurity</span><span class="w"> </span><span class="nt">-InputString</span><span class="w"> </span><span class="nv">$encodedValue</span><span class="w"> </span><span class="nt">-Key</span><span class="w"> </span><span class="s2">"YourKey"</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="proactive-monitoring">Proactive Monitoring</h2>

<p>Rather than waiting for something to break, a scheduled script can sweep your environment daily and flag issues before they become tickets:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Daily agent health check script</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-ExpandProperty</span><span class="w"> </span><span class="nx">Name</span><span class="w">
</span><span class="nv">$issues</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$computers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$info</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAInfo</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w">
            </span><span class="nv">$services</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="p">,</span><span class="w"> </span><span class="nx">LTSvcMon</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">

            </span><span class="c"># Check for issues</span><span class="w">
            </span><span class="nv">$problems</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">

            </span><span class="kr">if</span><span class="w"> </span><span class="p">((</span><span class="nv">$services</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Name</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'LTService'</span><span class="p">)</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Running'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$problems</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s2">"LTService not running"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="kr">if</span><span class="w"> </span><span class="p">((</span><span class="nv">$services</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Name</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'LTSvcMon'</span><span class="p">)</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Running'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$problems</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s2">"LTSvcMon not running"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="c"># Check last contact (within last 30 minutes)</span><span class="w">
            </span><span class="nv">$lastContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">datetime</span><span class="p">]</span><span class="nv">$info</span><span class="o">.</span><span class="nf">LastSuccessfulContact</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">((</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$lastContact</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="p">[</span><span class="n">timespan</span><span class="p">]::</span><span class="n">FromMinutes</span><span class="p">(</span><span class="mi">30</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$problems</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s2">"Last contact: </span><span class="si">$(</span><span class="nv">$lastContact</span><span class="si">)</span><span class="s2"> (over 30 min ago)"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="c"># Check for recent errors</span><span class="w">
            </span><span class="nv">$recentErrors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Last</span><span class="w"> </span><span class="nx">50</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s2">"CRITICAL|FATAL"</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$recentErrors</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$problems</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s2">"Critical errors in log"</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$problems</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                    </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="w">
                    </span><span class="nx">Issues</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$problems</span><span class="w"> </span><span class="err">-</span><span class="nx">join</span><span class="w"> </span><span class="s1">'; '</span><span class="w">
                    </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">ID</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">Stop</span><span class="w">

        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$result</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$issues</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">$result</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$issues</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">Issues</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Unable to connect or get agent info: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w">
            </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Report issues</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$issues</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$issues</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"AgentIssues-</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">" -NoTypeInformation

    # Send email alert
    Send-MailMessage -To "</span><span class="n">alerts</span><span class="err">@</span><span class="nx">yourmsp.com</span><span class="s2">" </span><span class="se">`</span><span class="s2">
                     -From "</span><span class="nx">monitoring</span><span class="err">@</span><span class="nx">yourmsp.com</span><span class="s2">" </span><span class="se">`</span><span class="s2">
                     -Subject "</span><span class="nx">ConnectWise</span><span class="w"> </span><span class="nx">Automate</span><span class="w"> </span><span class="nx">Agent</span><span class="w"> </span><span class="nx">Issues:</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nv">$issues</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> problems found"</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-Body</span><span class="w"> </span><span class="p">(</span><span class="nv">$issues</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-String</span><span class="p">)</span><span class="w"> </span><span class="err">`</span><span class="w">
                     </span><span class="nt">-SmtpServer</span><span class="w"> </span><span class="s2">"smtp.yourmsp.com"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"All agents healthy!"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>For automated remediation on top of detection, see <code class="language-plaintext highlighter-rouge">Register-CWAAHealthCheckTask</code> and the <a href="/powershell/rmm/2024/03/01/self-healing-connectwise-automate-agents.html">Self-Healing Agents</a> post.</p>

<hr />

<h2 id="custom-decision-tree">Custom Decision Tree</h2>

<p>The module includes <code class="language-plaintext highlighter-rouge">Repair-CWAA</code>, which handles escalating remediation natively (restart, reinstall, fresh install). If you need custom logic beyond what <code class="language-plaintext highlighter-rouge">Repair-CWAA</code> provides – different escalation thresholds, additional diagnostic steps, or environment-specific checks – you can build your own decision tree:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Repair-CWAAAgent</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">param</span><span class="p">([</span><span class="n">string</span><span class="p">]</span><span class="nv">$ComputerName</span><span class="p">)</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Diagnosing </span><span class="nv">$ComputerName</span><span class="s2">..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">

    </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$ComputerName</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Step 1: Are services running?</span><span class="w">
        </span><span class="nv">$services</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="p">,</span><span class="w"> </span><span class="nx">LTSvcMon</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$services</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-contains</span><span class="w"> </span><span class="s1">'Stopped'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Issue: Services stopped. Attempting restart..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
            </span><span class="n">Restart-CWAA</span><span class="w">
            </span><span class="nx">Start-Sleep</span><span class="w"> </span><span class="nx">10</span><span class="w">
            </span><span class="kr">return</span><span class="w"> </span><span class="s2">"Services restarted"</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Step 2: Can we reach the server?</span><span class="w">
        </span><span class="nv">$info</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAInfo</span><span class="w">
        </span><span class="nv">$portOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-CWAAPort</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$info</span><span class="o">.</span><span class="nf">Server</span><span class="w">
        </span><span class="nx">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$portOutput</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s1">'Connection failed'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Issue: Connectivity problem detected"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">

            </span><span class="c"># Check if proxy needed</span><span class="w">
            </span><span class="nv">$proxySettings</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAProxy</span><span class="w">
            </span><span class="nx">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$proxySettings</span><span class="o">.</span><span class="nf">ProxyServerURL</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  No proxy configured. May need proxy settings."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
                </span><span class="kr">return</span><span class="w"> </span><span class="s2">"Connectivity issue - check firewall/proxy"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Step 3: Check for errors</span><span class="w">
        </span><span class="nv">$errors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAError</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Last</span><span class="w"> </span><span class="nx">50</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s2">"ERROR|CRITICAL"</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$errors</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Issue: Errors in log"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">

            </span><span class="c"># Common fixable errors</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$errors</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s2">"heartbeat"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Invoke-CWAACommand</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"Send Status"</span><span class="w">
                </span><span class="kr">return</span><span class="w"> </span><span class="s2">"Heartbeat issue - status sent"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Step 4: Nothing obvious found</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Issue unclear. Recommending reinstall."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
        </span><span class="kr">return</span><span class="w"> </span><span class="s2">"Needs manual investigation or reinstall"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Use it</span><span class="w">
</span><span class="n">Repair-CWAAAgent</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="s2">"PROBLEM-PC"</span><span class="w">
</span></code></pre></div></div>

<p>For most environments, the built-in <code class="language-plaintext highlighter-rouge">Repair-CWAA</code> with <code class="language-plaintext highlighter-rouge">Register-CWAAHealthCheckTask</code> handles the common cases without custom scripting.</p>

<hr />

<h2 id="multi-machine-repair-script">Multi-Machine Repair Script</h2>

<p>When you need to push a restart-then-reinstall sequence to a list of machines:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Emergency Agent Repair Script</span><span class="w">
</span><span class="kr">param</span><span class="p">(</span><span class="w">
    </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$ComputerName</span><span class="p">,</span><span class="w">

    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://automate.yourmsp.com"</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$InstallerToken</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$ComputerName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">=== Processing </span><span class="nv">$computer</span><span class="s2"> ==="</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">

    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="c"># Quick restart</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Attempting service restart..."</span><span class="w">
            </span><span class="n">Restart-CWAA</span><span class="w">
            </span><span class="nx">Start-Sleep</span><span class="w"> </span><span class="nx">15</span><span class="w">

            </span><span class="c"># Check if it worked</span><span class="w">
            </span><span class="nv">$services</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="p">,</span><span class="w"> </span><span class="nx">LTSvcMon</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$services</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-contains</span><span class="w"> </span><span class="s1">'Stopped'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Services still stopped. Attempting full reinstall..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">

                </span><span class="n">Redo-CWAA</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">Server</span><span class="w"> </span><span class="se">`
</span><span class="w">                          </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">InstallerToken</span><span class="w"> </span><span class="se">`
</span><span class="w">                          </span><span class="nt">-Force</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Services running. Forcing status update..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
                </span><span class="n">Invoke-CWAACommand</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"Send Status"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"[OK] </span><span class="nv">$computer</span><span class="s2"> processed successfully"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"[FAIL] </span><span class="nv">$computer</span><span class="s2"> failed: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<p><strong>Getting started:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
</span></code></pre></div></div>

<p>Full function reference and examples: <a href="https://github.com/christaylorcodes/ConnectWiseAutomateAgent">GitHub Repository</a></p>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="Troubleshooting" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Automate" /><category term="RMM" /><category term="Troubleshooting" /><category term="Diagnostics" /><category term="MSP" /><summary type="html"><![CDATA[Structured troubleshooting workflows for ConnectWise Automate agents using PowerShell. Covers offline agents, intermittent connectivity, stopped services, log analysis, bulk health checks, and automated remediation scripts you can run from your workstation.]]></summary></entry><entry><title type="html">Self-Healing Agents: Automated Health Monitoring for ConnectWise Automate</title><link href="https://christaylor.codes/powershell/rmm/2024/03/01/self-healing-connectwise-automate-agents.html" rel="alternate" type="text/html" title="Self-Healing Agents: Automated Health Monitoring for ConnectWise Automate" /><published>2024-03-01T10:00:00+00:00</published><updated>2024-03-01T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/rmm/2024/03/01/self-healing-connectwise-automate-agents</id><content type="html" xml:base="https://christaylor.codes/powershell/rmm/2024/03/01/self-healing-connectwise-automate-agents.html"><![CDATA[<h2 id="what-this-covers">What This Covers</h2>

<p>The ConnectWiseAutomateAgent module includes a health check and auto-remediation system. You deploy it once per endpoint, and the agent monitors and repairs itself on a schedule. Services hang, it restarts them. Agent gets uninstalled, it reinstalls. Server address changes, it corrects itself. Every action gets logged to the Windows Event Log.</p>

<p>This post walks through the system: what each function does, how to set it up, how the escalation logic works, and how to monitor the results.</p>

<hr />

<h2 id="the-health-check-system">The Health Check System</h2>

<p>The system is built from five functions that work together:</p>

<table>
  <thead>
    <tr>
      <th>Function</th>
      <th>Role</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Test-CWAAHealth</code></td>
      <td>Read-only health assessment – checks installation, services, last check-in, server connectivity</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Repair-CWAA</code></td>
      <td>Escalating remediation – restart, reinstall, or fresh install depending on severity</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Register-CWAAHealthCheckTask</code></td>
      <td>Creates a Windows scheduled task that runs <code class="language-plaintext highlighter-rouge">Repair-CWAA</code> on an interval</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Unregister-CWAAHealthCheckTask</code></td>
      <td>Removes the scheduled task when you no longer need it</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Test-CWAAServerConnectivity</code></td>
      <td>Verifies the Automate server is online before attempting remediation</td>
    </tr>
  </tbody>
</table>

<p>The design is straightforward: check first, then apply the minimum fix needed. A service restart is tried before a reinstall. A reinstall is tried before a fresh install. The system never takes a heavier action than necessary, and every action gets logged to the Windows Event Log for a complete audit trail.</p>

<hr />

<h2 id="running-a-health-check">Running a Health Check</h2>

<p><code class="language-plaintext highlighter-rouge">Test-CWAAHealth</code> is the diagnostic starting point. It’s read-only – it never modifies the agent, services, or registry. Safe to run at any time, from any script, without risk.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Basic health check</span><span class="w">
</span><span class="n">Test-CWAAHealth</span><span class="w">
</span></code></pre></div></div>

<p>This returns a <code class="language-plaintext highlighter-rouge">PSCustomObject</code> with these properties:</p>

<table>
  <thead>
    <tr>
      <th>Property</th>
      <th>Type</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AgentInstalled</code></td>
      <td>Boolean</td>
      <td><code class="language-plaintext highlighter-rouge">True</code> if the LTService service exists</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ServicesRunning</code></td>
      <td>Boolean</td>
      <td><code class="language-plaintext highlighter-rouge">True</code> if both LTService and LTSvcMon are running</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">LastContact</code></td>
      <td>DateTime</td>
      <td>Timestamp of the agent’s last successful status report</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">LastHeartbeat</code></td>
      <td>DateTime</td>
      <td>Timestamp of the last heartbeat sent to the server</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ServerAddress</code></td>
      <td>String</td>
      <td>The server URL(s) the agent is configured to use</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ServerMatch</code></td>
      <td>Boolean/Null</td>
      <td>Whether the installed server matches the <code class="language-plaintext highlighter-rouge">-Server</code> parameter (null if not tested)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ServerReachable</code></td>
      <td>Boolean/Null</td>
      <td>Whether the server responded to a connectivity test (null if not tested)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Healthy</code></td>
      <td>Boolean</td>
      <td><code class="language-plaintext highlighter-rouge">True</code> only when installed, running, and has a recent contact timestamp</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">Healthy</code> property is the quick answer: <code class="language-plaintext highlighter-rouge">True</code> means the agent is installed, both services are running, and it has successfully contacted the server at least once. Everything else is detail for when <code class="language-plaintext highlighter-rouge">Healthy</code> is <code class="language-plaintext highlighter-rouge">False</code> and you need to know why.</p>

<p>For a more thorough check, add server validation and connectivity testing:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Full health check with server validation and connectivity test</span><span class="w">
</span><span class="n">Test-CWAAHealth</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="s1">'https://automate.domain.com'</span><span class="w"> </span><span class="nt">-TestServerConnectivity</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">-Server</code> parameter compares the provided URL against the agent’s configured server and populates the <code class="language-plaintext highlighter-rouge">ServerMatch</code> property. The <code class="language-plaintext highlighter-rouge">-TestServerConnectivity</code> switch sends a request to the server’s <code class="language-plaintext highlighter-rouge">agent.aspx</code> endpoint and populates <code class="language-plaintext highlighter-rouge">ServerReachable</code>. Both add useful context but aren’t required for the basic assessment.</p>

<p>Use it in scripts for conditional logic:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$health</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-CWAAHealth</span><span class="w"> </span><span class="nt">-TestServerConnectivity</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$health</span><span class="o">.</span><span class="nf">Healthy</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Agent unhealthy on </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="s2">"</span><span class="w">
    </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"  Installed: </span><span class="si">$(</span><span class="nv">$health</span><span class="o">.</span><span class="nf">AgentInstalled</span><span class="si">)</span><span class="s2">"</span><span class="w">
    </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"  Services:  </span><span class="si">$(</span><span class="nv">$health</span><span class="o">.</span><span class="nf">ServicesRunning</span><span class="si">)</span><span class="s2">"</span><span class="w">
    </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"  Last seen: </span><span class="si">$(</span><span class="nv">$health</span><span class="o">.</span><span class="nf">LastContact</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="setting-up-automated-health-checks">Setting Up Automated Health Checks</h2>

<p>Running <code class="language-plaintext highlighter-rouge">Test-CWAAHealth</code> manually is useful for troubleshooting, but the real value is unattended monitoring. <code class="language-plaintext highlighter-rouge">Register-CWAAHealthCheckTask</code> creates a Windows scheduled task that runs <code class="language-plaintext highlighter-rouge">Repair-CWAA</code> at a configurable interval (default: every 6 hours).</p>

<h3 id="checkup-mode-existing-agents">Checkup Mode (Existing Agents)</h3>

<p>If the agent is already installed and you want to keep it healthy:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Register-CWAAHealthCheckTask</span><span class="w"> </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s1">'abc123def456'</span><span class="w">
</span></code></pre></div></div>

<p>This is “Checkup” mode. The scheduled task will restart services or reinstall the agent if it goes offline, but it can’t perform a fresh install if the agent is completely removed – it needs existing configuration or backup settings to work from.</p>

<h3 id="install-mode-full-self-healing">Install Mode (Full Self-Healing)</h3>

<p>For complete self-healing capability, including installing the agent from scratch if it gets uninstalled:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Register-CWAAHealthCheckTask</span><span class="w"> </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s1">'abc123def456'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Server</span><span class="w"> </span><span class="s1">'https://automate.domain.com'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nx">42</span><span class="w">
</span></code></pre></div></div>

<p>This is “Install” mode. The task has everything it needs to deploy a fresh agent if one isn’t found, making the machine truly self-healing.</p>

<h3 id="customizing-the-schedule">Customizing the Schedule</h3>

<p>The defaults work for most environments, but you can adjust:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Register-CWAAHealthCheckTask</span><span class="w"> </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s1">'abc123def456'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Server</span><span class="w"> </span><span class="s1">'https://automate.domain.com'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nx">42</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-IntervalHours</span><span class="w"> </span><span class="nx">12</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-TaskName</span><span class="w"> </span><span class="s1">'MyHealthCheck'</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">-IntervalHours</code> parameter accepts values from 1 to 168 (one week). The task includes a random delay equal to the interval, so if you deploy this across hundreds of machines, they won’t all hit the server at the same time.</p>

<p>Details about the scheduled task:</p>

<ul>
  <li><strong>Runs as SYSTEM</strong> with highest privileges – no stored user credentials to expire</li>
  <li><strong>1-hour execution timeout</strong> – prevents stuck tasks from blocking subsequent runs</li>
  <li><strong>Runs on battery</strong> – laptops are covered even when unplugged</li>
  <li><strong>IgnoreNew instance policy</strong> – if the previous run is still going, the new one is skipped</li>
  <li><strong>Backs up agent config</strong> via <code class="language-plaintext highlighter-rouge">New-CWAABackup</code> before registering, so recovery settings are available</li>
</ul>

<p>If your InstallerToken changes (e.g., token rotation), re-run the command with the new token and the task updates automatically. Use <code class="language-plaintext highlighter-rouge">-Force</code> to recreate unconditionally.</p>

<h3 id="removing-the-task">Removing the Task</h3>

<p>When you no longer need automated monitoring on a machine:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Unregister-CWAAHealthCheckTask</span><span class="w">
</span></code></pre></div></div>

<p>Or with a custom task name:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Unregister-CWAAHealthCheckTask</span><span class="w"> </span><span class="nt">-TaskName</span><span class="w"> </span><span class="s1">'MyHealthCheck'</span><span class="w">
</span></code></pre></div></div>

<p>The function returns a result object with a <code class="language-plaintext highlighter-rouge">Removed</code> boolean. If the task doesn’t exist, it writes a warning and returns gracefully – no errors thrown.</p>

<hr />

<h2 id="how-repair-cwaa-escalates">How Repair-CWAA Escalates</h2>

<p><code class="language-plaintext highlighter-rouge">Repair-CWAA</code> is the engine behind the scheduled task. It checks the agent’s state and applies the minimum remediation needed, escalating only when lighter fixes fail. Here’s the decision tree:</p>

<h3 id="stage-1-agent-is-healthy">Stage 1: Agent Is Healthy</h3>

<p>If the agent is installed, both services are running, and the last check-in is within the <code class="language-plaintext highlighter-rouge">HoursRestart</code> threshold (default: 2 hours), no action is taken. Event ID 4000 is logged as informational.</p>

<h3 id="stage-2-services-need-a-restart">Stage 2: Services Need a Restart</h3>

<p>If the last check-in or heartbeat is older than <code class="language-plaintext highlighter-rouge">HoursRestart</code> (default: 2 hours), the function first verifies the server is reachable using <code class="language-plaintext highlighter-rouge">Test-CWAAServerConnectivity</code>. If the server is down, remediation is skipped – no point restarting services when the server isn’t there (Event ID 4008).</p>

<p>If the server is reachable, it restarts both services (<code class="language-plaintext highlighter-rouge">LTService</code> and <code class="language-plaintext highlighter-rouge">LTSvcMon</code>) and waits up to 2 minutes, polling every 2 seconds for the agent to check in. If it recovers, Event ID 4001 (Information) is logged.</p>

<h3 id="stage-3-full-reinstall">Stage 3: Full Reinstall</h3>

<p>If the restart didn’t fix it and the last check-in is older than <code class="language-plaintext highlighter-rouge">HoursReinstall</code> (default: 120 hours / 5 days), the agent is reinstalled via <code class="language-plaintext highlighter-rouge">Redo-CWAA</code>. This preserves the agent ID and configuration where possible. Event ID 4002 tracks the escalation.</p>

<h3 id="stage-4-server-mismatch">Stage 4: Server Mismatch</h3>

<p>If a <code class="language-plaintext highlighter-rouge">-Server</code> parameter was provided and the installed agent points to a different server, the agent is reinstalled with the correct server address. Event ID 4004 logs the mismatch and correction.</p>

<h3 id="stage-5-corrupt-configuration">Stage 5: Corrupt Configuration</h3>

<p>If <code class="language-plaintext highlighter-rouge">Get-CWAAInfo</code> fails to read the agent’s registry configuration, the agent is uninstalled for a clean reinstall on the next cycle. Event ID 4009 tracks this.</p>

<h3 id="stage-6-agent-not-installed">Stage 6: Agent Not Installed</h3>

<p>If no <code class="language-plaintext highlighter-rouge">LTService</code> service exists at all, the function attempts a fresh install. In Install mode (Server + LocationID + InstallerToken), it has everything needed. In Checkup mode, it tries to recover from backup settings created by <code class="language-plaintext highlighter-rouge">New-CWAABackup</code>. If no settings are available, it logs an error (Event ID 4009) and reports the failure.</p>

<h3 id="custom-thresholds">Custom Thresholds</h3>

<p>The defaults (restart after 2 hours, reinstall after 5 days) work for most environments, but you can tune them:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># More aggressive: restart after 1 hour, reinstall after 2 days</span><span class="w">
</span><span class="n">Repair-CWAA</span><span class="w"> </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s1">'token'</span><span class="w"> </span><span class="nt">-HoursRestart</span><span class="w"> </span><span class="nt">-1</span><span class="w"> </span><span class="nt">-HoursReinstall</span><span class="w"> </span><span class="nt">-48</span><span class="w">

</span><span class="c"># More conservative: restart after 4 hours, reinstall after 10 days</span><span class="w">
</span><span class="n">Repair-CWAA</span><span class="w"> </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s1">'token'</span><span class="w"> </span><span class="nt">-HoursRestart</span><span class="w"> </span><span class="nt">-4</span><span class="w"> </span><span class="nt">-HoursReinstall</span><span class="w"> </span><span class="nt">-240</span><span class="w">
</span></code></pre></div></div>

<p>The hour values are expressed as negative numbers (offsets from the current time). <code class="language-plaintext highlighter-rouge">-2</code> means “2 hours ago.”</p>

<h3 id="duplicate-process-protection">Duplicate Process Protection</h3>

<p><code class="language-plaintext highlighter-rouge">Repair-CWAA</code> kills any existing <code class="language-plaintext highlighter-rouge">Repair-CWAA</code> PowerShell processes before starting, so overlapping scheduled task runs don’t compete with each other. It matches on the process command line and excludes its own PID.</p>

<hr />

<h2 id="event-log-integration">Event Log Integration</h2>

<p>Every remediation action is logged to the Windows Event Log under:</p>

<ul>
  <li><strong>Log:</strong> Application</li>
  <li><strong>Source:</strong> ConnectWiseAutomateAgent</li>
</ul>

<p>This gives you a queryable audit trail that works with your existing Windows monitoring tools – SIEM, Event Log forwarding, <code class="language-plaintext highlighter-rouge">Get-WinEvent</code>, whatever you’re already using.</p>

<h3 id="health-check-event-ids">Health Check Event IDs</h3>

<table>
  <thead>
    <tr>
      <th>Event ID</th>
      <th>Entry Type</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4000</td>
      <td>Information</td>
      <td>Agent is healthy, no action needed</td>
    </tr>
    <tr>
      <td>4001</td>
      <td>Warning/Information</td>
      <td>Agent offline, services restarted (Warning). Agent recovered after restart (Information).</td>
    </tr>
    <tr>
      <td>4002</td>
      <td>Warning/Information</td>
      <td>Restart failed, reinstalling (Warning). Reinstall completed (Information).</td>
    </tr>
    <tr>
      <td>4003</td>
      <td>Warning/Information</td>
      <td>Agent not installed, attempting install (Warning). Install completed (Information).</td>
    </tr>
    <tr>
      <td>4004</td>
      <td>Warning/Information</td>
      <td>Server mismatch detected (Warning). Reinstalled with correct server (Information).</td>
    </tr>
    <tr>
      <td>4008</td>
      <td>Error</td>
      <td>Server unreachable, remediation skipped</td>
    </tr>
    <tr>
      <td>4009</td>
      <td>Error</td>
      <td>General failure (unreadable config, install failed, reinstall failed)</td>
    </tr>
    <tr>
      <td>4020</td>
      <td>Information</td>
      <td>Health check scheduled task registered</td>
    </tr>
    <tr>
      <td>4022</td>
      <td>Error</td>
      <td>Failed to register scheduled task</td>
    </tr>
    <tr>
      <td>4030</td>
      <td>Information</td>
      <td>Health check scheduled task removed</td>
    </tr>
    <tr>
      <td>4032</td>
      <td>Error</td>
      <td>Failed to remove scheduled task</td>
    </tr>
  </tbody>
</table>

<h3 id="querying-health-events">Querying Health Events</h3>

<p>Pull recent health check activity:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># All health-related events from the last 7 days</span><span class="w">
</span><span class="n">Get-WinEvent</span><span class="w"> </span><span class="nt">-FilterHashtable</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">LogName</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s1">'Application'</span><span class="w">
    </span><span class="nx">ProviderName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'ConnectWiseAutomateAgent'</span><span class="w">
    </span><span class="nx">Id</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="mi">4000</span><span class="p">,</span><span class="w"> </span><span class="mi">4001</span><span class="p">,</span><span class="w"> </span><span class="mi">4002</span><span class="p">,</span><span class="w"> </span><span class="mi">4003</span><span class="p">,</span><span class="w"> </span><span class="mi">4004</span><span class="p">,</span><span class="w"> </span><span class="mi">4008</span><span class="p">,</span><span class="w"> </span><span class="mi">4009</span><span class="w">
    </span><span class="nx">StartTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nx">Get</span><span class="err">-</span><span class="nx">Date</span><span class="err">).</span><span class="nx">AddDays</span><span class="err">(-</span><span class="mi">7</span><span class="err">)</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nx">TimeCreated</span><span class="p">,</span><span class="w"> </span><span class="nx">Id</span><span class="p">,</span><span class="w"> </span><span class="nx">LevelDisplayName</span><span class="p">,</span><span class="w"> </span><span class="nx">Message</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w">
</span></code></pre></div></div>

<p>Filter to just problems:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Only warnings and errors</span><span class="w">
</span><span class="n">Get-WinEvent</span><span class="w"> </span><span class="nt">-FilterHashtable</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">LogName</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'Application'</span><span class="w">
    </span><span class="nx">ProviderName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'ConnectWiseAutomateAgent'</span><span class="w">
    </span><span class="nx">Level</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="w">  </span><span class="c"># 2 = Error, 3 = Warning</span><span class="w">
    </span><span class="nx">StartTime</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nx">Get</span><span class="err">-</span><span class="nx">Date</span><span class="err">).</span><span class="nx">AddDays</span><span class="err">(-</span><span class="mi">30</span><span class="err">)</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nx">TimeCreated</span><span class="p">,</span><span class="w"> </span><span class="nx">Id</span><span class="p">,</span><span class="w"> </span><span class="nx">LevelDisplayName</span><span class="p">,</span><span class="w"> </span><span class="nx">Message</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w">
</span></code></pre></div></div>

<p>Check if the scheduled task is firing:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Task registration and removal events</span><span class="w">
</span><span class="n">Get-WinEvent</span><span class="w"> </span><span class="nt">-FilterHashtable</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">LogName</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'Application'</span><span class="w">
    </span><span class="nx">ProviderName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'ConnectWiseAutomateAgent'</span><span class="w">
    </span><span class="nx">Id</span><span class="w">           </span><span class="o">=</span><span class="w"> </span><span class="mi">4020</span><span class="p">,</span><span class="w"> </span><span class="mi">4022</span><span class="p">,</span><span class="w"> </span><span class="mi">4030</span><span class="p">,</span><span class="w"> </span><span class="mi">4032</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nx">TimeCreated</span><span class="p">,</span><span class="w"> </span><span class="nx">Id</span><span class="p">,</span><span class="w"> </span><span class="nx">Message</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w">
</span></code></pre></div></div>

<p>These events integrate with Windows Event Log forwarding, so you can aggregate health check results across your fleet into a central collector or SIEM.</p>

<hr />

<h2 id="verifying-server-connectivity">Verifying Server Connectivity</h2>

<p>Before <code class="language-plaintext highlighter-rouge">Repair-CWAA</code> attempts any remediation, it checks whether the server is reachable. You can also use <code class="language-plaintext highlighter-rouge">Test-CWAAServerConnectivity</code> directly for troubleshooting:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Detailed result</span><span class="w">
</span><span class="n">Test-CWAAServerConnectivity</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="s1">'https://automate.domain.com'</span><span class="w">
</span></code></pre></div></div>

<p>This returns a <code class="language-plaintext highlighter-rouge">PSCustomObject</code> per server with <code class="language-plaintext highlighter-rouge">Server</code>, <code class="language-plaintext highlighter-rouge">Available</code>, <code class="language-plaintext highlighter-rouge">Version</code>, and <code class="language-plaintext highlighter-rouge">ErrorMessage</code> properties. The function queries the server’s <code class="language-plaintext highlighter-rouge">/LabTech/agent.aspx</code> endpoint and validates the response matches the expected version format.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Simple boolean for scripts</span><span class="w">
</span><span class="n">Test-CWAAServerConnectivity</span><span class="w"> </span><span class="nt">-Quiet</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">-Quiet</code> switch returns <code class="language-plaintext highlighter-rouge">$True</code> if the server is reachable, <code class="language-plaintext highlighter-rouge">$False</code> otherwise. If no <code class="language-plaintext highlighter-rouge">-Server</code> is provided, it auto-discovers the server from the installed agent’s configuration or backup settings.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Pipeline from installed agent</span><span class="w">
</span><span class="n">Get-CWAAInfo</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Test-CWAAServerConnectivity</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="complete-setup-deploy-and-enable-self-healing">Complete Setup: Deploy and Enable Self-Healing</h2>

<p>Here’s a complete script that installs the agent, registers the health check task, and verifies everything is working. This is what a planned deployment looks like when you want self-healing from day one.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># --- Configuration ---</span><span class="w">
</span><span class="nv">$InstallerToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'YourGeneratedInstallerToken'</span><span class="w">
</span><span class="nv">$Server</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://automate.domain.com'</span><span class="w">
</span><span class="nv">$LocationID</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="mi">42</span><span class="w">

</span><span class="c"># --- Load the module ---</span><span class="w">
</span><span class="p">[</span><span class="n">Net.ServicePointManager</span><span class="p">]::</span><span class="n">SecurityProtocol</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Net.ServicePointManager</span><span class="p">]::</span><span class="n">SecurityProtocol</span><span class="w"> </span><span class="o">-bor</span><span class="w"> </span><span class="p">[</span><span class="n">Net.SecurityProtocolType</span><span class="p">]::</span><span class="nx">Tls12</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$Module</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'ConnectWiseAutomateAgent'</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">Update-Module</span><span class="w"> </span><span class="nv">$Module</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">Install-Module</span><span class="w"> </span><span class="nv">$Module</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">AllUsers</span><span class="w"> </span><span class="nt">-SkipPublisherCheck</span><span class="w"> </span><span class="p">}</span><span class="w">

    </span><span class="n">Get-Module</span><span class="w"> </span><span class="nv">$Module</span><span class="w"> </span><span class="nt">-ListAvailable</span><span class="w"> </span><span class="o">|</span><span class="w">
        </span><span class="n">Sort-Object</span><span class="w"> </span><span class="nx">Version</span><span class="w"> </span><span class="nt">-Descending</span><span class="w"> </span><span class="o">|</span><span class="w">
        </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="o">|</span><span class="w">
        </span><span class="n">Import-Module</span><span class="w"> </span><span class="o">*</span><span class="err">&gt;</span><span class="bp">$null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$URI</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://raw.githubusercontent.com/christaylorcodes/ConnectWiseAutomateAgent/main/ConnectWiseAutomateAgent.ps1'</span><span class="w">
    </span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Net.WebClient</span><span class="p">)</span><span class="o">.</span><span class="nf">DownloadString</span><span class="p">(</span><span class="nv">$URI</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Invoke-Expression</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># --- Step 1: Install the agent ---</span><span class="w">
</span><span class="n">Redo-CWAA</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$Server</span><span class="w"> </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nv">$LocationID</span><span class="w"> </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="nv">$InstallerToken</span><span class="w">

</span><span class="c"># --- Step 2: Register the health check ---</span><span class="w">
</span><span class="n">Register-CWAAHealthCheckTask</span><span class="w"> </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="nv">$InstallerToken</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$Server</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nv">$LocationID</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-IntervalHours</span><span class="w"> </span><span class="nx">6</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Force</span><span class="w">

</span><span class="c"># --- Step 3: Verify ---</span><span class="w">
</span><span class="nv">$health</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-CWAAHealth</span><span class="w"> </span><span class="nt">-TestServerConnectivity</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$health</span><span class="o">.</span><span class="nf">Healthy</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Agent is healthy. Server reachable: </span><span class="si">$(</span><span class="nv">$health</span><span class="o">.</span><span class="nf">ServerReachable</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s1">'Agent not yet healthy. The scheduled task will remediate automatically.'</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>After this runs, the machine is covered:</p>

<ol>
  <li>The agent is installed and connected to your server.</li>
  <li>Every 6 hours, the scheduled task checks if the agent is healthy.</li>
  <li>If services are hung, they get restarted.</li>
  <li>If the agent has been offline for 5+ days, it gets reinstalled.</li>
  <li>If someone uninstalls the agent, it gets reinstalled from scratch.</li>
  <li>Every action is logged to the Windows Event Log for auditing.</li>
</ol>

<p>You can deploy this via Group Policy (GPO), Intune, your existing RMM, or any tool that can run PowerShell on endpoints. The health check task handles the rest.</p>

<hr />

<h2 id="what-this-looks-like-at-scale">What This Looks Like at Scale</h2>

<p>The math here is simple. If you manage 1,000 endpoints and 2% of agents have issues in a given month, that’s 20 manual interventions. Each one involves identifying the problem, connecting to the machine, diagnosing, fixing, and verifying. With the health check system, those interventions handle themselves before they become tickets.</p>

<p>What changes in practice:</p>

<ul>
  <li><strong>Reduced time to recovery.</strong> An offline agent that would sit unnoticed for days gets restarted within hours. If that doesn’t work, it’s reinstalled within the week. No human required.</li>
  <li><strong>Fewer reactive tickets.</strong> The most common agent issue – hung services – is the first thing the system tries to fix. A service restart resolves the majority of offline agents, and it happens automatically on the next scheduled check.</li>
  <li><strong>Complete audit trail.</strong> Every action taken (or skipped) is in the Windows Event Log with a categorized Event ID. When someone asks “what happened to that machine?”, you have the answer.</li>
  <li><strong>Consistent behavior.</strong> The same escalation logic runs on every machine. No variation between technicians, no forgotten steps.</li>
  <li><strong>Server-aware remediation.</strong> The system checks if the server is reachable before attempting fixes. If the Automate server itself is down, it doesn’t waste time restarting agents that can’t connect anyway.</li>
</ul>

<p>The health check system doesn’t replace monitoring – it supplements it. Your Automate server still tracks online/offline status. But now, when it flags an agent as offline, there’s already a process running on that endpoint working to bring it back.</p>

<hr />

<p><strong>Getting started:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
</span></code></pre></div></div>

<p>Full function reference and examples: <a href="https://github.com/christaylorcodes/ConnectWiseAutomateAgent">GitHub Repository</a></p>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="RMM" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Automate" /><category term="RMM" /><category term="Health Monitoring" /><category term="Self-Healing" /><category term="MSP" /><summary type="html"><![CDATA[Deploy a scheduled health check that monitors and repairs ConnectWise Automate agents automatically. Escalating remediation restarts services, reinstalls agents, and logs every action to the Windows Event Log for auditing.]]></summary></entry><entry><title type="html">Mass Agent Deployment: Deploying ConnectWise Automate Agents at Scale</title><link href="https://christaylor.codes/powershell/deployment/2024/02/26/mass-agent-deployment-connectwise-automate.html" rel="alternate" type="text/html" title="Mass Agent Deployment: Deploying ConnectWise Automate Agents at Scale" /><published>2024-02-26T10:00:00+00:00</published><updated>2024-02-26T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/deployment/2024/02/26/mass-agent-deployment-connectwise-automate</id><content type="html" xml:base="https://christaylor.codes/powershell/deployment/2024/02/26/mass-agent-deployment-connectwise-automate.html"><![CDATA[<h2 id="what-this-covers">What This Covers</h2>

<p>The ConnectWiseAutomateAgent module handles mass deployment of ConnectWise Automate agents via PowerShell remoting. You build an inventory from Active Directory (AD), run parallel installations across your target machines, retry failures, and verify the results – all from a single console.</p>

<p>This post walks through a full deployment: preparation, the deployment script, failure handling, verification, and a few advanced patterns for staged rollouts and integration with other tools.</p>

<hr />

<h2 id="why-script-it">Why Script It</h2>

<p>Deploying a Remote Monitoring and Management (RMM) agent to a handful of machines is straightforward. Deploying to hundreds is where the usual approaches start to fall apart:</p>

<ul>
  <li>GPO deployments that fail silently</li>
  <li>Login scripts that run… sometimes</li>
  <li>Email campaigns with download links (chaos)</li>
  <li>Sneakernet USB drives (it’s 2025, really?)</li>
  <li>One-by-one remote installations (enjoy your weekend)</li>
</ul>

<p>PowerShell remoting gives you parallel execution, per-machine error handling, and a CSV report at the end. The ConnectWiseAutomateAgent module provides the installation, configuration, and verification functions. Together, they turn a multi-day project into a scripted operation.</p>

<hr />

<h2 id="scenario-multi-site-client-onboarding">Scenario: Multi-Site Client Onboarding</h2>

<p>The examples below use a representative scenario. Adjust the values to match your environment.</p>

<p><strong>Infrastructure:</strong></p>
<ul>
  <li>500 Windows 10/11 workstations</li>
  <li>50 Windows Servers (mix of 2016/2019/2022)</li>
  <li>3 office locations (each with different proxy requirements)</li>
  <li>Active Directory environment</li>
  <li>Some machines behind restrictive firewalls</li>
</ul>

<p><strong>Requirements:</strong></p>
<ul>
  <li>Agent must be hidden from Add/Remove Programs</li>
  <li>Custom display name: “SecureWatch Monitoring”</li>
  <li>Different LocationIDs per office</li>
  <li>Proxy configuration for Site B</li>
  <li>No user disruption during deployment</li>
  <li>Complete within business hours</li>
</ul>

<hr />

<h2 id="phase-1-preparation">Phase 1: Preparation</h2>

<h3 id="install-the-module">Install the Module</h3>

<p>First, install on your deployment machine (or server):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install from PowerShell Gallery</span><span class="w">
</span><span class="n">Install-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w"> </span><span class="nt">-Force</span><span class="w">

</span><span class="c"># Verify installation</span><span class="w">
</span><span class="n">Get-Command</span><span class="w"> </span><span class="nt">-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
</span></code></pre></div></div>

<h3 id="gather-information">Gather Information</h3>

<p>You’ll need:</p>
<ul>
  <li><strong>Server URL</strong>: <code class="language-plaintext highlighter-rouge">https://automate.yourmsp.com</code></li>
  <li><strong>Installer Token</strong>: Get from ConnectWise Automate (more secure than passwords)</li>
  <li><strong>LocationIDs</strong>:
    <ul>
      <li>Site A (HQ): 100</li>
      <li>Site B (Warehouse): 101</li>
      <li>Site C (Remote Office): 102</li>
    </ul>
  </li>
</ul>

<h3 id="create-computer-inventory">Create Computer Inventory</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Export computers from Active Directory</span><span class="w">
</span><span class="nv">$siteA</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-SearchBase</span><span class="w"> </span><span class="s2">"OU=Workstations,OU=SiteA,DC=client,DC=com"</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Select-Object</span><span class="w"> </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'ComputerName'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="p">}},</span><span class="w">
                  </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'LocationID'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="mi">100</span><span class="p">}},</span><span class="w">
                  </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'Proxy'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="bp">$null</span><span class="p">}}</span><span class="w">

</span><span class="nv">$siteB</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-SearchBase</span><span class="w"> </span><span class="s2">"OU=Workstations,OU=SiteB,DC=client,DC=com"</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Select-Object</span><span class="w"> </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'ComputerName'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="p">}},</span><span class="w">
                  </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'LocationID'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="mi">101</span><span class="p">}},</span><span class="w">
                  </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'Proxy'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="s1">'http://proxy.siteb.client.com:8080'</span><span class="p">}}</span><span class="w">

</span><span class="nv">$siteC</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-SearchBase</span><span class="w"> </span><span class="s2">"OU=Workstations,OU=SiteC,DC=client,DC=com"</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Select-Object</span><span class="w"> </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'ComputerName'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="p">}},</span><span class="w">
                  </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'LocationID'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="mi">102</span><span class="p">}},</span><span class="w">
                  </span><span class="p">@{</span><span class="nx">N</span><span class="o">=</span><span class="s1">'Proxy'</span><span class="p">;</span><span class="nx">E</span><span class="o">=</span><span class="p">{</span><span class="bp">$null</span><span class="p">}}</span><span class="w">

</span><span class="c"># Combine all sites</span><span class="w">
</span><span class="nv">$allComputers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$siteA</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$siteB</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$siteC</span><span class="w">

</span><span class="c"># Export for tracking</span><span class="w">
</span><span class="nv">$allComputers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"Deployment-Inventory.csv"</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Total machines to deploy: </span><span class="si">$(</span><span class="nv">$allComputers</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="phase-2-deployment-script">Phase 2: Deployment Script</h2>

<h3 id="the-master-deployment-script">The Master Deployment Script</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">&lt;#
</span><span class="cs">.SYNOPSIS</span><span class="cm">
    Mass deployment script for ConnectWise Automate agents

</span><span class="cs">.DESCRIPTION</span><span class="cm">
    Deploys agents to multiple computers in parallel with full error handling
    and reporting.

</span><span class="cs">.PARAMETER</span><span class="cm"> ComputerList
    Path to CSV file with columns: ComputerName, LocationID, Proxy

</span><span class="cs">.PARAMETER</span><span class="cm"> Server
    ConnectWise Automate server URL

</span><span class="cs">.PARAMETER</span><span class="cm"> InstallerToken
    Installer token from ConnectWise Automate

</span><span class="cs">.PARAMETER</span><span class="cm"> ThrottleLimit
    Number of parallel deployments (default: 20)

</span><span class="cs">.EXAMPLE</span><span class="cm">
    .\Deploy-Agents.ps1 -ComputerList "Deployment-Inventory.csv" -Server "https://automate.msp.com" -InstallerToken "abc123"
#&gt;</span><span class="w">

</span><span class="kr">param</span><span class="p">(</span><span class="w">
    </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ComputerList</span><span class="p">,</span><span class="w">

    </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Server</span><span class="p">,</span><span class="w">

    </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$InstallerToken</span><span class="p">,</span><span class="w">

    </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$ThrottleLimit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">20</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="c"># Import computer list</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Import-Csv</span><span class="w"> </span><span class="nv">$ComputerList</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Starting deployment to </span><span class="si">$(</span><span class="nv">$computers</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> computers..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Parallel deployments: </span><span class="nv">$ThrottleLimit</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">

</span><span class="c"># Create results tracking</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">ComputerName</span><span class="w">
    </span><span class="nv">$locationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LocationID</span><span class="w">
    </span><span class="nv">$proxy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Proxy</span><span class="w">
    </span><span class="nv">$server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">Server</span><span class="w">
    </span><span class="nv">$token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">using</span><span class="p">:</span><span class="nv">InstallerToken</span><span class="w">

    </span><span class="nv">$result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
        </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$locationID</span><span class="w">
        </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Unknown'</span><span class="w">
        </span><span class="nx">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">''</span><span class="w">
        </span><span class="nx">Duration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="w">
        </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">''</span><span class="w">
        </span><span class="nx">Timestamp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Get</span><span class="err">-</span><span class="nx">Date</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Test connectivity first</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="p">(</span><span class="n">Test-Connection</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-Count</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="nt">-Quiet</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$result</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Offline'</span><span class="w">
            </span><span class="nv">$result</span><span class="o">.</span><span class="nf">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Computer not reachable'</span><span class="w">
            </span><span class="kr">return</span><span class="w"> </span><span class="nv">$result</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Deploy agent</span><span class="w">
        </span><span class="nv">$startTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w">

        </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="kr">param</span><span class="p">(</span><span class="nv">$srv</span><span class="p">,</span><span class="w"> </span><span class="nv">$tok</span><span class="p">,</span><span class="w"> </span><span class="nv">$loc</span><span class="p">,</span><span class="w"> </span><span class="nv">$prx</span><span class="p">)</span><span class="w">

            </span><span class="c"># Import module</span><span class="w">
            </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w">

            </span><span class="c"># Configure proxy if specified (proxy is set at module level, not on Install-CWAA)</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$prx</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Set-CWAAProxy</span><span class="w"> </span><span class="nt">-ProxyServerURL</span><span class="w"> </span><span class="nv">$prx</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="c"># Build installation parameters</span><span class="w">
            </span><span class="nv">$installParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
                </span><span class="nx">Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$srv</span><span class="w">
                </span><span class="nx">InstallerToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tok</span><span class="w">
                </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$loc</span><span class="w">
                </span><span class="nx">Hide</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
                </span><span class="nx">Rename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"SecureWatch Monitoring"</span><span class="w">
                </span><span class="nx">ErrorAction</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Stop'</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="c"># Install</span><span class="w">
            </span><span class="n">Install-CWAA</span><span class="w"> </span><span class="err">@</span><span class="nx">installParams</span><span class="w">

            </span><span class="c"># Get agent info</span><span class="w">
            </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nx">5</span><span class="w">
            </span><span class="nv">$info</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWAAInfo</span><span class="w">

            </span><span class="kr">return</span><span class="w"> </span><span class="nv">$info</span><span class="o">.</span><span class="nf">ID</span><span class="w">

        </span><span class="p">}</span><span class="w"> </span><span class="nt">-ArgumentList</span><span class="w"> </span><span class="nv">$server</span><span class="p">,</span><span class="w"> </span><span class="nv">$token</span><span class="p">,</span><span class="w"> </span><span class="nv">$locationID</span><span class="p">,</span><span class="w"> </span><span class="nv">$proxy</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">Stop</span><span class="w">

        </span><span class="nv">$endTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w">
        </span><span class="nv">$duration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$endTime</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$startTime</span><span class="p">)</span><span class="o">.</span><span class="nf">TotalSeconds</span><span class="w">

        </span><span class="nv">$result</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Success'</span><span class="w">
        </span><span class="nv">$result</span><span class="o">.</span><span class="nf">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Agent deployed successfully'</span><span class="w">
        </span><span class="nv">$result</span><span class="o">.</span><span class="nf">Duration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">math</span><span class="p">]::</span><span class="n">Round</span><span class="p">(</span><span class="nv">$duration</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">

    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$result</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Failed'</span><span class="w">
        </span><span class="nv">$result</span><span class="o">.</span><span class="nf">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Progress output</span><span class="w">
    </span><span class="nv">$status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$result</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Success'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s1">'✓'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s1">'✗'</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="nv">$status</span><span class="s2"> </span><span class="nv">$computer</span><span class="s2"> - </span><span class="si">$(</span><span class="nv">$result</span><span class="o">.</span><span class="nf">Status</span><span class="si">)</span><span class="s2"> (</span><span class="si">$(</span><span class="nv">$result</span><span class="o">.</span><span class="nf">Duration</span><span class="si">)</span><span class="s2">s)"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$result</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Success'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s1">'Green'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s1">'Red'</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$result</span><span class="w">

</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="nv">$ThrottleLimit</span><span class="w">

</span><span class="c"># Generate report</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">=== Deployment Summary ==="</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Total: </span><span class="si">$(</span><span class="nv">$results</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Success: </span><span class="si">$(</span><span class="p">(</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Success'</span><span class="p">)</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Failed: </span><span class="si">$(</span><span class="p">(</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Failed'</span><span class="p">)</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Offline: </span><span class="si">$(</span><span class="p">(</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Offline'</span><span class="p">)</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">

</span><span class="c"># Export results</span><span class="w">
</span><span class="nv">$reportFile</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Deployment-Results-</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd-HHmmss'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">"
</span><span class="nv">$results</span><span class="s2"> | Export-Csv </span><span class="nv">$reportFile</span><span class="s2"> -NoTypeInformation
Write-Host "</span><span class="err">`</span><span class="n">nDetailed</span><span class="w"> </span><span class="nx">results:</span><span class="w"> </span><span class="nv">$reportFile</span><span class="s2">" -ForegroundColor Cyan

# Show failures
</span><span class="nv">$failures</span><span class="s2"> = </span><span class="nv">$results</span><span class="s2"> | Where-Object { </span><span class="bp">$_</span><span class="s2">.Status -ne 'Success' }
if (</span><span class="nv">$failures</span><span class="s2">) {
    Write-Host "</span><span class="se">`n</span><span class="o">===</span><span class="w"> </span><span class="n">Failed</span><span class="w"> </span><span class="nx">Deployments</span><span class="w"> </span><span class="o">===</span><span class="s2">" -ForegroundColor Red
    </span><span class="nv">$failures</span><span class="s2"> | Format-Table ComputerName, Status, Message -AutoSize
}

# Calculate average deployment time
</span><span class="nv">$successTimes</span><span class="s2"> = </span><span class="nv">$results</span><span class="s2"> | Where-Object Status -eq 'Success' | Measure-Object -Property Duration -Average
if (</span><span class="nv">$successTimes</span><span class="s2">.Count -gt 0) {
    Write-Host "</span><span class="err">`</span><span class="n">nAverage</span><span class="w"> </span><span class="nx">deployment</span><span class="w"> </span><span class="nx">time:</span><span class="w"> </span><span class="err">$</span><span class="p">([</span><span class="n">math</span><span class="p">]::</span><span class="n">Round</span><span class="p">(</span><span class="nv">$successTimes</span><span class="o">.</span><span class="nf">Average</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="si">)</span><span class="s2">) seconds"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="n">Green</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="run-the-deployment">Run the Deployment</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="n">\Deploy-Agents.ps1</span><span class="w"> </span><span class="nt">-ComputerList</span><span class="w"> </span><span class="s2">"Deployment-Inventory.csv"</span><span class="w"> </span><span class="se">`
</span><span class="w">                    </span><span class="nt">-Server</span><span class="w"> </span><span class="s2">"https://automate.yourmsp.com"</span><span class="w"> </span><span class="se">`
</span><span class="w">                    </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s2">"your-secure-token-here"</span><span class="w"> </span><span class="se">`
</span><span class="w">                    </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="nx">25</span><span class="w">
</span></code></pre></div></div>

<p><strong>Expected output</strong>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Starting deployment to 500 computers...
Parallel deployments: 25
✓ WKS001 - Success (45s)
✓ WKS002 - Success (43s)
✗ WKS003 - Failed (Connection timeout)
✓ WKS004 - Success (47s)
...

=== Deployment Summary ===
Total: 500
Success: 487
Failed: 8
Offline: 5

Average deployment time: 44 seconds
</code></pre></div></div>

<hr />

<h2 id="phase-3-handling-failures">Phase 3: Handling Failures</h2>

<p>Some machines won’t succeed on the first pass. Common reasons: machine was powered off, WinRM wasn’t enabled, or a firewall rule blocked the connection. The deployment script exports a CSV with per-machine results, so retrying is straightforward.</p>

<h3 id="retry-failed-deployments">Retry Failed Deployments</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Load previous results</span><span class="w">
</span><span class="nv">$previousResults</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Import-Csv</span><span class="w"> </span><span class="s2">"Deployment-Results-20251103-140522.csv"</span><span class="w">

</span><span class="c"># Get failures</span><span class="w">
</span><span class="nv">$failures</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$previousResults</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Success'</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="c"># Create retry list</span><span class="w">
</span><span class="nv">$retryList</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$failures</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">ComputerName</span><span class="p">,</span><span class="w"> </span><span class="nx">LocationID</span><span class="p">,</span><span class="w"> </span><span class="nx">Proxy</span><span class="w">

</span><span class="c"># Export retry list</span><span class="w">
</span><span class="nv">$retryList</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"Deployment-Retry.csv"</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">

</span><span class="c"># Run deployment again on failures only</span><span class="w">
</span><span class="o">.</span><span class="n">\Deploy-Agents.ps1</span><span class="w"> </span><span class="nt">-ComputerList</span><span class="w"> </span><span class="s2">"Deployment-Retry.csv"</span><span class="w"> </span><span class="se">`
</span><span class="w">                    </span><span class="nt">-Server</span><span class="w"> </span><span class="s2">"https://automate.yourmsp.com"</span><span class="w"> </span><span class="se">`
</span><span class="w">                    </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s2">"your-secure-token-here"</span><span class="w"> </span><span class="se">`
</span><span class="w">                    </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="nx">10</span><span class="w">
</span></code></pre></div></div>

<h3 id="manual-investigation">Manual Investigation</h3>

<p>For machines that fail repeatedly, this script checks the basics – network reachability, WinRM status, and whether an agent is already installed:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$problemComputers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Import-Csv</span><span class="w"> </span><span class="s2">"Deployment-Retry.csv"</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Failed'</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$pc</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$problemComputers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">=== Investigating </span><span class="si">$(</span><span class="nv">$pc</span><span class="o">.</span><span class="nf">ComputerName</span><span class="si">)</span><span class="s2"> ==="</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">

    </span><span class="c"># Check connectivity</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="n">Test-Connection</span><span class="w"> </span><span class="nv">$pc</span><span class="o">.</span><span class="nf">ComputerName</span><span class="w"> </span><span class="nt">-Count</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="nt">-Quiet</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Online"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">

        </span><span class="c"># Check WinRM</span><span class="w">
        </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$pc</span><span class="o">.</span><span class="nf">ComputerName</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">Stop</span><span class="w">
            </span><span class="nx">Write-Host</span><span class="w"> </span><span class="s2">"  WinRM: OK"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  WinRM: FAILED - </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Check if agent already installed</span><span class="w">
        </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$existing</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$pc</span><span class="o">.</span><span class="nf">ComputerName</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">Get-Service</span><span class="w"> </span><span class="nx">LTService</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Agent already installed: </span><span class="si">$(</span><span class="nv">$existing</span><span class="o">.</span><span class="nf">Status</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Agent not installed"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Gray</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Offline"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="phase-4-verification">Phase 4: Verification</h2>

<p>After deployment completes, verify independently that agents are installed and reporting. Don’t just trust the deployment script’s output – confirm it.</p>

<h3 id="verify-all-deployments">Verify All Deployments</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Verification script</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Import-Csv</span><span class="w"> </span><span class="s2">"Deployment-Inventory.csv"</span><span class="w">

</span><span class="nv">$verification</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="nt">-Parallel</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">ComputerName</span><span class="w">

    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$info</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w">
            </span><span class="n">Get-CWAAInfo</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">Stop</span><span class="w">

        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">AgentInstalled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
            </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">ID</span><span class="w">
            </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">LocationID</span><span class="w">
            </span><span class="nx">LastContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">LastSuccessfulContact</span><span class="w">
            </span><span class="nx">Version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$info</span><span class="err">.</span><span class="nx">Version</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Verified'</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
            </span><span class="nx">AgentInstalled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
            </span><span class="nx">AgentID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">LocationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">LastContact</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">Version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'N/A'</span><span class="w">
            </span><span class="nx">Status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Not Verified'</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="nt">-ThrottleLimit</span><span class="w"> </span><span class="mi">30</span><span class="w">

</span><span class="c"># Report</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">=== Verification Results ==="</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Cyan</span><span class="w">
</span><span class="nv">$verified</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$verification</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Verified'</span><span class="p">)</span><span class="o">.</span><span class="nf">Count</span><span class="w">
</span><span class="nv">$total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$verification</span><span class="o">.</span><span class="nf">Count</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Verified: </span><span class="nv">$verified</span><span class="s2"> / </span><span class="nv">$total</span><span class="s2"> (</span><span class="si">$(</span><span class="p">[</span><span class="n">math</span><span class="p">]::</span><span class="n">Round</span><span class="p">(</span><span class="nv">$verified</span><span class="n">/</span><span class="nv">$total</span><span class="o">*</span><span class="nx">100</span><span class="p">,</span><span class="nx">1</span><span class="p">)</span><span class="si">)</span><span class="s2">%)"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">

</span><span class="c"># Export</span><span class="w">
</span><span class="nv">$verification</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"Deployment-Verification-</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyyMMdd'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">" -NoTypeInformation

# Check-in with ConnectWise Automate
Write-Host "</span><span class="err">`</span><span class="n">nCheck</span><span class="w"> </span><span class="nx">your</span><span class="w"> </span><span class="nx">ConnectWise</span><span class="w"> </span><span class="nx">Automate</span><span class="w"> </span><span class="nx">console</span><span class="w"> </span><span class="nx">for</span><span class="w"> </span><span class="nx">agent</span><span class="w"> </span><span class="nx">check-ins...</span><span class="s2">"
</span></code></pre></div></div>

<hr />

<h2 id="advanced-techniques">Advanced Techniques</h2>

<h3 id="staged-rollout">Staged Rollout</h3>

<p>Deploying everything at once is tempting but risky. A staged rollout lets you validate the process on a small group before committing to the full inventory.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Deploy to 10% first (pilot group)</span><span class="w">
</span><span class="nv">$pilot</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$allComputers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">50</span><span class="w">
</span><span class="nv">$pilot</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"Wave1-Pilot.csv"</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">

</span><span class="c"># After pilot success, deploy to 50%</span><span class="w">
</span><span class="nv">$wave2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$allComputers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Skip</span><span class="w"> </span><span class="nx">50</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">225</span><span class="w">
</span><span class="nv">$wave2</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"Wave2-Main.csv"</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">

</span><span class="c"># Finally deploy remainder</span><span class="w">
</span><span class="nv">$wave3</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$allComputers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-Skip</span><span class="w"> </span><span class="nx">275</span><span class="w">
</span><span class="nv">$wave3</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="s2">"Wave3-Final.csv"</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span></code></pre></div></div>

<p>Run each wave through the same <code class="language-plaintext highlighter-rouge">Deploy-Agents.ps1</code> script and review the results before moving on. If your pilot wave surfaces a consistent failure (wrong LocationID, firewall rule, etc.), you can fix it before it affects 450 machines instead of 50.</p>

<h3 id="integration-with-pdq-deploy">Integration with PDQ Deploy</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create PDQ Deploy package that runs:</span><span class="w">
</span><span class="n">powershell.exe</span><span class="w"> </span><span class="nt">-ExecutionPolicy</span><span class="w"> </span><span class="nx">Bypass</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"&amp; {
    Install-Module ConnectWiseAutomateAgent -Force
    Install-CWAA -Server 'https://automate.msp.com' </span><span class="se">`</span><span class="s2">
                 -InstallerToken 'token' </span><span class="se">`</span><span class="s2">
                 -LocationID 100 </span><span class="se">`</span><span class="s2">
                 -Hide </span><span class="se">`</span><span class="s2">
                 -Rename 'SecureWatch Monitoring'
}"</span><span class="w">
</span></code></pre></div></div>

<h3 id="group-policy-startup-script">Group Policy Startup Script</h3>

<p>Save this as a startup script:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># GPO-Agent-Install.ps1</span><span class="w">
</span><span class="nv">$marker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"C:\Windows\Temp\CWAAInstalled.txt"</span><span class="w">

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="p">(</span><span class="n">Test-Path</span><span class="w"> </span><span class="nv">$marker</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Determine LocationID based on OU</span><span class="w">
        </span><span class="nv">$computerDN</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">([</span><span class="n">ADSI</span><span class="p">]</span><span class="s2">"LDAP://</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="s2">"</span><span class="p">)</span><span class="o">.</span><span class="nf">distinguishedName</span><span class="w">
        </span><span class="nv">$locationID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">switch</span><span class="w"> </span><span class="nt">-Regex</span><span class="w"> </span><span class="p">(</span><span class="nv">$computerDN</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="s1">'OU=SiteA'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="s1">'OU=SiteB'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="mi">101</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="s1">'OU=SiteC'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="mi">102</span><span class="w"> </span><span class="p">}</span><span class="w">
            </span><span class="n">default</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Install module</span><span class="w">
        </span><span class="n">Install-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">AllUsers</span><span class="w">

        </span><span class="c"># Install agent</span><span class="w">
        </span><span class="n">Install-CWAA</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="s1">'https://automate.msp.com'</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="s1">'your-token'</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nv">$locationID</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-Hide</span><span class="w"> </span><span class="se">`
</span><span class="w">                     </span><span class="nt">-Rename</span><span class="w"> </span><span class="s1">'SecureWatch Monitoring'</span><span class="w">

        </span><span class="c"># Mark as installed</span><span class="w">
        </span><span class="s2">"Installed </span><span class="si">$(</span><span class="n">Get-Date</span><span class="p">)</span><span class="s2">" | Out-File </span><span class="nv">$marker</span><span class="s2">
    }
    catch {
        "</span><span class="n">Failed:</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-File</span><span class="w"> </span><span class="s2">"C:\Windows\Temp\CWAAInstallError.txt"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="what-to-expect-at-scale">What to Expect at Scale</h2>

<p>The numbers you’ll see depend on your environment – network speed, WinRM configuration, server load, and how many machines are actually online. As a rough guide for a 500-machine deployment with 20-25 parallel threads:</p>

<ul>
  <li><strong>Total time</strong>: Under an hour for the deployment pass</li>
  <li><strong>Success rate</strong>: 95%+ if WinRM is pre-configured and machines are online</li>
  <li><strong>Per-machine install time</strong>: 40-50 seconds on average</li>
  <li><strong>Common failures</strong>: WinRM not enabled, machine powered off, firewall blocking the connection</li>
</ul>

<p>The remaining 2-5% typically need one retry pass or manual attention (enable WinRM, power on the machine, open a firewall port). The retry workflow in Phase 3 handles most of these.</p>

<p>For comparison, manual deployment of 500 agents at 3-5 minutes each is roughly 25-40 hours of technician time. The scripted approach compresses that into under an hour of execution plus review time.</p>

<hr />

<h2 id="best-practices">Best Practices</h2>

<ul>
  <li><strong>Test on a pilot group first.</strong> Deploy to 5-10% before the full rollout.</li>
  <li><strong>Enable WinRM ahead of time.</strong> Use GPO to enable PowerShell remoting across the target environment.</li>
  <li><strong>Stagger your deployments.</strong> Don’t send 500 simultaneous installs at your Automate server.</li>
  <li><strong>Monitor server load.</strong> Keep an eye on your Automate server’s resources during large deployments.</li>
  <li><strong>Keep installer tokens secure.</strong> Don’t commit them to Git. Use a credential vault or pass them as parameters.</li>
  <li><strong>Verify check-ins.</strong> Confirm agents appear in the Automate console after deployment, not just in your script output.</li>
  <li><strong>Document LocationIDs.</strong> Maintain a mapping of sites to IDs. You’ll reference it every time you onboard a new site.</li>
  <li><strong>Save deployment reports.</strong> The CSV exports are useful for billing, documentation, and troubleshooting later.</li>
</ul>

<hr />

<h2 id="troubleshooting-common-issues">Troubleshooting Common Issues</h2>

<h3 id="issue-access-denied-errors">Issue: “Access Denied” Errors</h3>

<p><strong>Cause</strong>: WinRM not enabled or insufficient permissions on the target machine.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Enable WinRM via GPO or run on each machine:</span><span class="w">
</span><span class="n">Enable-PSRemoting</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>

<h3 id="issue-module-not-found-on-remote-machines">Issue: “Module Not Found” on Remote Machines</h3>

<p><strong>Cause</strong>: The module isn’t installed on the target. <code class="language-plaintext highlighter-rouge">Invoke-Command</code> runs on the remote machine, so the module needs to be there.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Pre-install module via GPO or:</span><span class="w">
</span><span class="n">Invoke-Command</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="nt">-ScriptBlock</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Install-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">AllUsers</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="issue-proxy-authentication-failures">Issue: Proxy Authentication Failures</h3>

<p><strong>Cause</strong>: The target machine needs proxy configuration before the installer can reach the Automate server. Proxy is set at the module level, not as a parameter on <code class="language-plaintext highlighter-rouge">Install-CWAA</code>.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Set proxy at the module level (proxy is not a parameter of Install-CWAA)</span><span class="w">
</span><span class="nv">$proxyPass</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ConvertTo-SecureString</span><span class="w"> </span><span class="s2">"password"</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="n">Set-CWAAProxy</span><span class="w"> </span><span class="nt">-ProxyServerURL</span><span class="w"> </span><span class="s2">"http://proxy:8080"</span><span class="w"> </span><span class="se">`
</span><span class="w">              </span><span class="nt">-ProxyUsername</span><span class="w"> </span><span class="s2">"domain\user"</span><span class="w"> </span><span class="se">`
</span><span class="w">              </span><span class="nt">-ProxyPassword</span><span class="w"> </span><span class="nv">$proxyPass</span><span class="w">

</span><span class="n">Install-CWAA</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$server</span><span class="w"> </span><span class="se">`
</span><span class="w">             </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="nv">$token</span><span class="w"> </span><span class="se">`
</span><span class="w">             </span><span class="nt">-LocationID</span><span class="w"> </span><span class="nv">$loc</span><span class="w">
</span></code></pre></div></div>

<hr />

<p><strong>Getting started:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nx">ConnectWiseAutomateAgent</span><span class="w">
</span></code></pre></div></div>

<p>Full function reference and examples: <a href="https://github.com/christaylorcodes/ConnectWiseAutomateAgent">GitHub Repository</a></p>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="Deployment" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Automate" /><category term="RMM" /><category term="Deployment" /><category term="Automation" /><category term="MSP" /><summary type="html"><![CDATA[Deploy ConnectWise Automate agents to hundreds of machines in parallel using PowerShell remoting. Covers inventory, phased rollouts, failure handling, verification, and integration with GPO and PDQ Deploy.]]></summary></entry><entry><title type="html">Automating ConnectWise Automate Agent Management Through Control</title><link href="https://christaylor.codes/powershell/msp/connectwisecontrolapi/2024/02/19/automating-automate-agent-deployment.html" rel="alternate" type="text/html" title="Automating ConnectWise Automate Agent Management Through Control" /><published>2024-02-19T10:00:00+00:00</published><updated>2024-02-19T10:00:00+00:00</updated><id>https://christaylor.codes/powershell/msp/connectwisecontrolapi/2024/02/19/automating-automate-agent-deployment</id><content type="html" xml:base="https://christaylor.codes/powershell/msp/connectwisecontrolapi/2024/02/19/automating-automate-agent-deployment.html"><![CDATA[<h2 id="the-msp-tech-stack-integration-challenge">The MSP Tech Stack Integration Challenge</h2>

<p>Most MSPs run ConnectWise Control alongside ConnectWise Automate (formerly LabTech). Control provides fast remote access, while Automate handles monitoring, patching, and automation. But when Automate agents fail or need reinstallation, you need a way to reach the machine—and that’s where Control becomes critical.</p>

<p>The classic scenario: An Automate agent stops checking in. The machine is online (you can see it in Control), but you’ve lost your monitoring and automation capabilities. Manually connecting through Control, opening a command prompt, and reinstalling the agent works… once. When you’re facing dozens of failed agents across multiple clients, manual intervention doesn’t scale.</p>

<p>This article demonstrates how to combine ConnectWiseControlAPI with remote command execution to automate agent maintenance tasks at scale.</p>

<h2 id="the-architecture-why-control-is-the-perfect-delivery-mechanism">The Architecture: Why Control is the Perfect Delivery Mechanism</h2>

<p>ConnectWise Control excels at:</p>
<ul>
  <li><strong>Fast connection establishment</strong> - Agents connect immediately on boot</li>
  <li><strong>Firewall-friendly</strong> - Uses outbound connections on port 443</li>
  <li><strong>Cross-platform support</strong> - Windows, Mac, Linux</li>
  <li><strong>Command execution</strong> - Run scripts without interactive desktop</li>
</ul>

<p>Even when Automate agents fail, Control agents typically remain connected. This makes Control an ideal mechanism for delivering repair commands to machines with failed automation agents.</p>

<h2 id="use-case-automated-automate-agent-reinstallation">Use Case: Automated Automate Agent Reinstallation</h2>

<p>When an Automate agent fails, the standard fix is:</p>

<ol>
  <li>Connect to the machine via Control</li>
  <li>Open command prompt or PowerShell</li>
  <li>Download the Automate installer</li>
  <li>Run the installer with appropriate parameters</li>
  <li>Verify agent checks in successfully</li>
</ol>

<p>With ConnectWiseControlAPI, this entire process becomes scriptable.</p>

<h3 id="the-manual-process-old-way">The Manual Process (Old Way)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Log into Control web interface
2. Search for machine name
3. Click "Connect" on the session
4. Wait for connection
5. Open command prompt
6. Type: powershell -Command "iwr https://... | iex; Install-LTAgent..."
7. Wait for completion
8. Close connection
9. Repeat for next machine...
</code></pre></div></div>

<p>For one machine: 5-10 minutes. For 50 machines: <strong>4+ hours of tedious work</strong>.</p>

<h3 id="the-automated-process-new-way">The Automated Process (New Way)</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Find all machines with failed Automate agents</span><span class="w">
</span><span class="nv">$failedAgents</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-FailedAutomateAgents</span><span class="w">  </span><span class="c"># Your detection logic</span><span class="w">

</span><span class="c"># For each failed agent, find in Control and reinstall</span><span class="w">
</span><span class="nv">$failedAgents</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$session</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWCSession</span><span class="w"> </span><span class="nt">-Search</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">ComputerName</span><span class="w"> </span><span class="nt">-Limit</span><span class="w"> </span><span class="nx">1</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$session</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Reinstall Automate agent via Control</span><span class="w">
        </span><span class="n">Invoke-AutomateReinstall</span><span class="w"> </span><span class="nt">-SessionID</span><span class="w"> </span><span class="nv">$session</span><span class="o">.</span><span class="nf">SessionID</span><span class="w"> </span><span class="nt">-InstallerToken</span><span class="w"> </span><span class="nv">$token</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>For 50 machines: <strong>5-10 minutes of script execution</strong> (unattended).</p>

<h2 id="the-complete-solution">The Complete Solution</h2>

<h3 id="step-1-identify-failed-agents">Step 1: Identify Failed Agents</h3>

<p>First, identify which machines need agent reinstallation. This might come from:</p>

<ul>
  <li>Automate’s own reporting (agents not checking in)</li>
  <li>ConnectWise Manage tickets</li>
  <li>Custom monitoring dashboards</li>
  <li>Scheduled reports</li>
</ul>

<p>For this example, assume you have a list of computer names:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Machines identified as having failed Automate agents</span><span class="w">
</span><span class="nv">$failedMachines</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
    </span><span class="s1">'CLIENT-A-PC01'</span><span class="p">,</span><span class="w">
    </span><span class="s1">'CLIENT-B-SERVER01'</span><span class="p">,</span><span class="w">
    </span><span class="s1">'CLIENT-C-LAPTOP05'</span><span class="w">
</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<h3 id="step-2-locate-machines-in-control">Step 2: Locate Machines in Control</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Connect to Control</span><span class="w">
</span><span class="n">Connect-CWC</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="s1">'https://control.yourdomain.com'</span><span class="w"> </span><span class="nt">-Credential</span><span class="w"> </span><span class="nv">$cred</span><span class="w">

</span><span class="c"># Find each machine in Control</span><span class="w">
</span><span class="nv">$controlSessions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$machine</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$failedMachines</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Searching for </span><span class="nv">$machine</span><span class="s2"> in Control..."</span><span class="w">

    </span><span class="nv">$session</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWCSession</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="nx">Access</span><span class="w"> </span><span class="nt">-Search</span><span class="w"> </span><span class="nv">$machine</span><span class="w"> </span><span class="nt">-Limit</span><span class="w"> </span><span class="nx">1</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$session</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Found: Session ID </span><span class="si">$(</span><span class="nv">$session</span><span class="o">.</span><span class="nf">SessionID</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
        </span><span class="nv">$session</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"  Not found in Control: </span><span class="nv">$machine</span><span class="s2">"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">Found </span><span class="si">$(</span><span class="nv">$controlSessions</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> of </span><span class="si">$(</span><span class="nv">$failedMachines</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> machines in Control"</span><span class="w">
</span></code></pre></div></div>

<h3 id="step-3-build-the-reinstall-command">Step 3: Build the Reinstall Command</h3>

<p>ConnectWise Automate provides installer tokens for agent deployment. Your command needs:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Automate installer command (PowerShell)</span><span class="w">
</span><span class="nv">$installerToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'YOUR-INSTALLER-TOKEN-HERE'</span><span class="w">
</span><span class="nv">$automateServer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'automate.yourdomain.com'</span><span class="w">

</span><span class="nv">$reinstallCommand</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
# Download Automate installer script
iwr -UseBasicParsing "https://bit.ly/LTPoSh" | iex

# Run reinstall (preserves existing configuration where possible)
Redo-LTService -InstallerToken '</span><span class="nv">$installerToken</span><span class="sh">' -Server '</span><span class="nv">$automateServer</span><span class="sh">' -Backup
"@</span><span class="w">
</span></code></pre></div></div>

<p><strong>Command Breakdown:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">iwr -UseBasicParsing</code> - Download installer script (works on older PowerShell versions)</li>
  <li><code class="language-plaintext highlighter-rouge">| iex</code> - Execute downloaded script</li>
  <li><code class="language-plaintext highlighter-rouge">Redo-LTService</code> - Function from downloaded script that handles reinstallation</li>
  <li><code class="language-plaintext highlighter-rouge">-InstallerToken</code> - Authenticates to Automate server</li>
  <li><code class="language-plaintext highlighter-rouge">-Server</code> - Specifies Automate server URL</li>
  <li><code class="language-plaintext highlighter-rouge">-Backup</code> - Backs up existing agent settings before reinstall</li>
</ul>

<h3 id="step-4-execute-reinstallation-via-control">Step 4: Execute Reinstallation via Control</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Execute reinstallation for each session</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$session</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$controlSessions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Reinstalling Automate on </span><span class="si">$(</span><span class="nv">$session</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2">..."</span><span class="w">

    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$commandResult</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-CWCCommand</span><span class="w"> </span><span class="se">`
</span><span class="w">            </span><span class="nt">-GUID</span><span class="w"> </span><span class="nv">$session</span><span class="o">.</span><span class="nf">SessionID</span><span class="w"> </span><span class="se">`
</span><span class="w">            </span><span class="nt">-Command</span><span class="w"> </span><span class="nv">$reinstallCommand</span><span class="w"> </span><span class="se">`
</span><span class="w">            </span><span class="nt">-PowerShell</span><span class="w"> </span><span class="se">`
</span><span class="w">            </span><span class="nt">-TimeOut</span><span class="w"> </span><span class="nx">300000</span><span class="w">  </span><span class="c"># 5 minute timeout for installation</span><span class="w">

        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$session</span><span class="err">.</span><span class="nx">Name</span><span class="w">
            </span><span class="nx">SessionID</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$session</span><span class="err">.</span><span class="nx">SessionID</span><span class="w">
            </span><span class="nx">Status</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s1">'Success'</span><span class="w">
            </span><span class="nx">Result</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="nv">$commandResult</span><span class="w">
            </span><span class="nx">Error</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"  Completed successfully"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Green</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"  Failed: </span><span class="bp">$_</span><span class="s2">"</span><span class="w">

        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$session</span><span class="err">.</span><span class="nx">Name</span><span class="w">
            </span><span class="nx">SessionID</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$session</span><span class="err">.</span><span class="nx">SessionID</span><span class="w">
            </span><span class="nx">Status</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s1">'Failed'</span><span class="w">
            </span><span class="nx">Result</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
            </span><span class="nx">Error</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">Exception</span><span class="err">.</span><span class="nx">Message</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Summary</span><span class="w">
</span><span class="nv">$successCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Success'</span><span class="w"> </span><span class="p">})</span><span class="o">.</span><span class="nf">Count</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="se">`n</span><span class="s2">Completed: </span><span class="nv">$successCount</span><span class="s2"> successful, </span><span class="si">$(</span><span class="nv">$results</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$successCount</span><span class="si">)</span><span class="s2"> failed"</span><span class="w">
</span></code></pre></div></div>

<h3 id="step-5-verification-and-reporting">Step 5: Verification and Reporting</h3>

<p>After execution, verify agents check back in:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Wait for agents to check in (typically 2-5 minutes)</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Waiting 5 minutes for agents to check in..."</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">300</span><span class="w">

</span><span class="c"># Check Automate for new check-ins</span><span class="w">
</span><span class="c"># (This would use ConnectWiseAutomateAPI or similar)</span><span class="w">
</span><span class="nv">$verifiedAgents</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Verify-AutomateAgentCheckIn</span><span class="w"> </span><span class="nt">-ComputerNames</span><span class="w"> </span><span class="nv">$controlSessions</span><span class="o">.</span><span class="nf">Name</span><span class="w">

</span><span class="c"># Generate report</span><span class="w">
</span><span class="nv">$report</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$checkInStatus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">ComputerName</span><span class="w"> </span><span class="nt">-in</span><span class="w"> </span><span class="nv">$verifiedAgents</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s1">'Checked In'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s1">'Not Verified'</span><span class="w"> </span><span class="p">}</span><span class="w">

    </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">ComputerName</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">ComputerName</span><span class="w">
        </span><span class="nx">InstallStatus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">Status</span><span class="w">
        </span><span class="nx">CheckInStatus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$checkInStatus</span><span class="w">
        </span><span class="nx">Error</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">Error</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Export for review</span><span class="w">
</span><span class="nv">$report</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"AutomateReinstall_</span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyy-MM-dd_HHmm'</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">" -NoTypeInformation

# Display summary
</span><span class="nv">$report</span><span class="s2"> | Format-Table -AutoSize
</span></code></pre></div></div>

<h2 id="the-complete-automated-script">The Complete Automated Script</h2>

<p>Here’s the full solution packaged as a reusable function:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-AutomateAgentReinstallViaControl</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="cm">&lt;#
    </span><span class="cs">.SYNOPSIS</span><span class="cm">
        Reinstall Automate agents using Control for delivery

    </span><span class="cs">.DESCRIPTION</span><span class="cm">
        Locates machines in ConnectWise Control and executes Automate agent
        reinstallation commands remotely.

    </span><span class="cs">.PARAMETER</span><span class="cm"> ComputerName
        Computer names to reinstall Automate agent on

    </span><span class="cs">.PARAMETER</span><span class="cm"> InstallerToken
        ConnectWise Automate installer token

    </span><span class="cs">.PARAMETER</span><span class="cm"> AutomateServer
        Automate server URL (e.g., 'automate.domain.com')

    </span><span class="cs">.PARAMETER</span><span class="cm"> ControlServer
        Control server URL (e.g., 'https://control.domain.com')

    </span><span class="cs">.PARAMETER</span><span class="cm"> Credential
        Control authentication credentials

    </span><span class="cs">.EXAMPLE</span><span class="cm">
        Invoke-AutomateAgentReinstallViaControl `
            -ComputerName 'PC01','PC02','PC03' `
            -InstallerToken 'abc123' `
            -AutomateServer 'automate.domain.com' `
            -ControlServer 'https://control.domain.com' `
            -Credential (Get-Credential)
    #&gt;</span><span class="w">

    </span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
    </span><span class="kr">param</span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$ComputerName</span><span class="p">,</span><span class="w">

        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$InstallerToken</span><span class="p">,</span><span class="w">

        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AutomateServer</span><span class="p">,</span><span class="w">

        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ControlServer</span><span class="p">,</span><span class="w">

        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">PSCredential</span><span class="p">]</span><span class="nv">$Credential</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="c"># Connect to Control</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Connect-CWC</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$ControlServer</span><span class="w"> </span><span class="nt">-Credential</span><span class="w"> </span><span class="nv">$Credential</span><span class="w">
        </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Connected to Control: </span><span class="nv">$ControlServer</span><span class="s2">"</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Failed to connect to Control: </span><span class="bp">$_</span><span class="s2">"</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Build reinstall command</span><span class="w">
    </span><span class="nv">$reinstallCommand</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
iwr -UseBasicParsing "https://bit.ly/LTPoSh" | iex
Redo-LTService -InstallerToken '</span><span class="nv">$InstallerToken</span><span class="sh">' -Server '</span><span class="nv">$AutomateServer</span><span class="sh">' -Backup
"@</span><span class="w">

    </span><span class="c"># Process each computer</span><span class="w">
    </span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$ComputerName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Processing </span><span class="nv">$computer</span><span class="s2">..."</span><span class="w">

        </span><span class="c"># Find in Control</span><span class="w">
        </span><span class="nv">$session</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CWCSession</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="nx">Access</span><span class="w"> </span><span class="nt">-Search</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="nt">-Limit</span><span class="w"> </span><span class="nx">1</span><span class="w">

        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$session</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"</span><span class="nv">$computer</span><span class="s2"> not found in Control"</span><span class="w">
            </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
                </span><span class="nx">Status</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s1">'NotFound'</span><span class="w">
                </span><span class="nx">Error</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="s1">'Session not found in Control'</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="kr">continue</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="c"># Execute reinstall</span><span class="w">
        </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-CWCCommand</span><span class="w"> </span><span class="se">`
</span><span class="w">                </span><span class="nt">-GUID</span><span class="w"> </span><span class="nv">$session</span><span class="o">.</span><span class="nf">SessionID</span><span class="w"> </span><span class="se">`
</span><span class="w">                </span><span class="nt">-Command</span><span class="w"> </span><span class="nv">$reinstallCommand</span><span class="w"> </span><span class="se">`
</span><span class="w">                </span><span class="nt">-PowerShell</span><span class="w"> </span><span class="se">`
</span><span class="w">                </span><span class="nt">-TimeOut</span><span class="w"> </span><span class="nx">300000</span><span class="w">

            </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
                </span><span class="nx">SessionID</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$session</span><span class="err">.</span><span class="nx">SessionID</span><span class="w">
                </span><span class="nx">Status</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s1">'Success'</span><span class="w">
                </span><span class="nx">Error</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Failed to reinstall on </span><span class="nv">$computer</span><span class="s2">: </span><span class="bp">$_</span><span class="s2">"</span><span class="w">
            </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
                </span><span class="nx">SessionID</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$session</span><span class="err">.</span><span class="nx">SessionID</span><span class="w">
                </span><span class="nx">Status</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s1">'Failed'</span><span class="w">
                </span><span class="nx">Error</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">Exception</span><span class="err">.</span><span class="nx">Message</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$results</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="usage">Usage</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Load module</span><span class="w">
</span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ConnectWiseControlAPI</span><span class="w">

</span><span class="c"># Define parameters</span><span class="w">
</span><span class="nv">$params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">ComputerName</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s1">'PC01'</span><span class="p">,</span><span class="w"> </span><span class="s1">'SERVER02'</span><span class="p">,</span><span class="w"> </span><span class="s1">'LAPTOP03'</span><span class="p">)</span><span class="w">
    </span><span class="nx">InstallerToken</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s1">'your-installer-token'</span><span class="w">
    </span><span class="nx">AutomateServer</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s1">'automate.yourdomain.com'</span><span class="w">
    </span><span class="nx">ControlServer</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://control.yourdomain.com'</span><span class="w">
    </span><span class="nx">Credential</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nx">Get</span><span class="err">-</span><span class="nx">Credential</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Execute reinstallation</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-AutomateAgentReinstallViaControl</span><span class="w"> </span><span class="err">@</span><span class="nx">params</span><span class="w">

</span><span class="c"># Review results</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w">
</span></code></pre></div></div>

<h2 id="advanced-considerations">Advanced Considerations</h2>

<h3 id="handling-different-client-environments">Handling Different Client Environments</h3>

<p>MSPs manage multiple clients with different Automate locations:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Client-specific installer tokens</span><span class="w">
</span><span class="nv">$clientConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="s1">'ClientA'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
        </span><span class="nx">InstallerToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'token-a-123'</span><span class="w">
        </span><span class="nx">LocationID</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="s1">'ClientB'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
        </span><span class="nx">InstallerToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'token-b-456'</span><span class="w">
        </span><span class="nx">LocationID</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Detect client from computer name and use appropriate token</span><span class="w">
</span><span class="nv">$clientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computerName</span><span class="o">.</span><span class="nf">Split</span><span class="p">(</span><span class="s1">'-'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span><span class="w">  </span><span class="c"># e.g., 'CLIENTA-PC01' -&gt; 'CLIENTA'</span><span class="w">
</span><span class="nv">$token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$clientConfig</span><span class="p">[</span><span class="nv">$clientID</span><span class="p">]</span><span class="o">.</span><span class="nf">InstallerToken</span><span class="w">
</span></code></pre></div></div>

<h3 id="logging-and-audit-trail">Logging and Audit Trail</h3>

<p>For compliance and troubleshooting:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Log all operations</span><span class="w">
</span><span class="nv">$logEntry</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
    </span><span class="nx">Timestamp</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nx">Get</span><span class="err">-</span><span class="nx">Date</span><span class="w">
    </span><span class="nx">ComputerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computer</span><span class="w">
    </span><span class="nx">Action</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s1">'AutomateReinstall'</span><span class="w">
    </span><span class="nx">SessionID</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$session</span><span class="err">.</span><span class="nx">SessionID</span><span class="w">
    </span><span class="nx">Status</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="nv">$status</span><span class="w">
    </span><span class="nx">ExecutedBy</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERNAME</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$logEntry</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s1">'C:\Logs\AutomateReinstalls.csv'</span><span class="w"> </span><span class="nt">-Append</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span></code></pre></div></div>

<h3 id="scheduled-automatic-remediation">Scheduled Automatic Remediation</h3>

<p>Combine with scheduled tasks for proactive remediation:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Detect failed agents (from your monitoring system)</span><span class="w">
</span><span class="nv">$failedAgents</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-FailedAutomateAgents</span><span class="w"> </span><span class="nt">-DaysOffline</span><span class="w"> </span><span class="nx">1</span><span class="w">

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$failedAgents</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c"># Attempt automated remediation</span><span class="w">
    </span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-AutomateAgentReinstallViaControl</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="nv">$failedAgents</span><span class="o">.</span><span class="nf">Name</span><span class="w">

    </span><span class="c"># Alert on failures</span><span class="w">
    </span><span class="nv">$failures</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Success'</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$failures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Send-MailMessage</span><span class="w"> </span><span class="nt">-Subject</span><span class="w"> </span><span class="s2">"Automate Remediation Failures"</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="p">(</span><span class="nv">$failures</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Html</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="real-world-results">Real-World Results</h2>

<p>After implementing automated agent reinstallation via Control:</p>

<ul>
  <li><strong>Reduced MTTR</strong> (Mean Time To Repair) from hours to minutes</li>
  <li><strong>Eliminated manual connection overhead</strong> for bulk agent issues</li>
  <li><strong>Improved agent uptime</strong> from ~95% to ~99.5%</li>
  <li><strong>Freed up ~10 hours per week</strong> of technician time</li>
  <li><strong>Enabled proactive remediation</strong> through scheduled automation</li>
</ul>

<h2 id="important-considerations">Important Considerations</h2>

<h3 id="security">Security</h3>

<ul>
  <li><strong>Protect installer tokens</strong> - Store securely, rotate regularly</li>
  <li><strong>Audit all operations</strong> - Log who executed what and when</li>
  <li><strong>Use dedicated service accounts</strong> - Don’t use personal credentials</li>
  <li><strong>Review before bulk execution</strong> - Test on a few machines first</li>
</ul>

<h3 id="testing">Testing</h3>

<p>Before deploying to production:</p>

<ol>
  <li>Test command manually in Control interface</li>
  <li>Test script against 1-2 non-critical machines</li>
  <li>Verify agent check-in after reinstallation</li>
  <li>Review logs for errors or unexpected behavior</li>
  <li>Document any edge cases discovered</li>
</ol>

<h3 id="error-handling">Error Handling</h3>

<p>Not all reinstallations succeed. Common failures:</p>

<ul>
  <li><strong>Machine offline</strong> - Control session shows online but is unresponsive</li>
  <li><strong>Permissions issues</strong> - Command needs elevation or different credentials</li>
  <li><strong>Network problems</strong> - Can’t download installer script</li>
  <li><strong>Conflicting software</strong> - Security software blocks execution</li>
</ul>

<p>Build retry logic and manual escalation for persistent failures.</p>

<h2 id="key-takeaways">Key Takeaways</h2>

<ul>
  <li><strong>Control provides reliable command delivery</strong> even when Automate agents fail</li>
  <li><strong>Automation scales manual processes</strong> from hours to minutes</li>
  <li><strong>Integration between tools</strong> multiplies their individual value</li>
  <li><strong>Proper error handling</strong> is critical for production automation</li>
  <li><strong>Security and auditing</strong> must be built in from the start</li>
</ul>

<h2 id="resources">Resources</h2>

<ul>
  <li><strong>ConnectWiseControlAPI</strong>: <a href="https://github.com/christaylorcodes/ConnectWiseControlAPI">GitHub</a></li>
  <li><strong>Automate Installer Documentation</strong>: Check your Automate server’s help docs</li>
  <li><strong>Related Functions</strong>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">Get-CWCSession</code> - <a href="https://github.com/christaylorcodes/ConnectWiseControlAPI/blob/master/Docs/Get-CWCSession.md">Docs</a></li>
      <li><code class="language-plaintext highlighter-rouge">Invoke-CWCCommand</code> - <a href="https://github.com/christaylorcodes/ConnectWiseControlAPI/blob/master/Docs/Invoke-CWCCommand.md">Docs</a></li>
    </ul>
  </li>
  <li><strong>Example Script</strong>: See <a href="https://github.com/christaylorcodes/ConnectWiseControlAPI/blob/master/Examples/ReinstallAutomate.ps1">ReinstallAutomate.ps1</a></li>
</ul>

<p>Have you automated agent management differently? Share your approaches in the GitHub discussions.</p>]]></content><author><name>Chris Taylor</name></author><category term="PowerShell" /><category term="MSP" /><category term="ConnectWiseControlAPI" /><category term="PowerShell" /><category term="ConnectWise" /><category term="Control" /><category term="Automate" /><category term="MSP" /><category term="Integration" /><category term="Agent Management" /><category term="Automation" /><summary type="html"><![CDATA[When Automate agents fail, use Control to reach machines and fix them remotely. This post shows how to connect both tools and automate agent maintenance that used to take hours.]]></summary></entry></feed>