<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: UiPath</title>
    <description>The latest articles on DEV Community by UiPath (uipath).</description>
    <link>https://dev.to/uipath</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3278%2F54a7a2e2-f933-423f-90ce-05b817dfe1ee.png</url>
      <title>DEV Community: UiPath</title>
      <link>https://dev.to/uipath</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uipath"/>
    <language>en</language>
    <item>
      <title>Managing SSH keys for work</title>
      <dc:creator>Cobus Bernard 🌩🥑🇿🇦 in 🇺🇸</dc:creator>
      <pubDate>Mon, 09 Nov 2026 21:40:34 +0000</pubDate>
      <link>https://dev.to/uipath/managing-ssh-keys-for-work-4cbi</link>
      <guid>https://dev.to/uipath/managing-ssh-keys-for-work-4cbi</guid>
      <description>&lt;p&gt;We recently started using Azure DevOps internally at UiPath for all non-public repos, and I was very excited when I saw it using Terraform in the PR workflows for setting up teams, repos, and managing everything. But more on that in a future post, first I had to sort out managing 3 different SSH keys on my work laptop without needing to specify which key to use for every &lt;code&gt;git&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;At UiPath, we use GitHub for any public or private product repos, and then Azure DevOps (ADO for short) for any other internal repos we want to share across teams. Add in my personal GitHub contributions, and I now have 3 different SSH keys to manage for commits and signing. &lt;/p&gt;

&lt;h2&gt;
  
  
  So what is the problem?
&lt;/h2&gt;

&lt;p&gt;Why a separate personal key at all, if one GitHub account would let me sign everything with the work key? Because I don't want to deal with the chaos when I need to revoke a key, and using only 1 means it would affect both my work and personal commits when I revoke it. I prefer my personal commits be tied to a key and an email that I own and control, and the work ones to a key that is linked with work. It keeps the line between myself and alter ego &lt;em&gt;"UiPath-Cobus"&lt;/em&gt; very clean. We've all seen the &lt;em&gt;"Opinions are my own and don't reflect those of my employer"&lt;/em&gt;  line on profiles, I'm curious how much that shielded you when you pushed some code with the wrong details.&lt;/p&gt;

&lt;p&gt;The end result is a setup where I can clone repos and push commits without having to think about which version of me is shown in the commit history.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan
&lt;/h2&gt;

&lt;p&gt;Initially it didn't create too much friction to keep track of which key to use as I almost never used my personal one. Then we started using ADO to package and share internal AI scripts, skills, and tools, so I decided it was time to sort this out properly after I last had to deal with it in &lt;a href="https://cobus.io/posts/2012-04-12/ssh-config-multiple-keys/" rel="noopener noreferrer"&gt;2012&lt;/a&gt;. Luckily, we also use 1Password at work, so it matched my home setup where I use the SSH agent to manage my SSH keys, I just had to store them in the correct vault.&lt;/p&gt;

&lt;p&gt;I knew roughly what I wanted the git behaviour to be, but I needed to write it out clearly enough to use in a prompt, so I did a rough mapping of some examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;remote URL                               ssh key       email / signing key
───────────────────────────────────────  ────────────  ───────────────────
git@github.com:UiPath/*                  work          work (explicit)
git@github.com:uipath-labs/*             work          work (explicit)
git@github.com:cobusbernard/*            work          personal
git@github.com:anyone-else/*             work          personal
git@ssh.dev.azure.com:v3/uipath/*        azure (RSA)   work (default)
https://dev.azure.com/uipath/*           n/a (GCM)     work (default)
no remote yet                            n/a           work (default)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To set up this mapping required editing both &lt;code&gt;~/.ssh/config&lt;/code&gt; and &lt;code&gt;~/.gitconfig&lt;/code&gt; (with additional &lt;code&gt;~/.gitconfig-&amp;lt;flavour&amp;gt;&lt;/code&gt;ones I'll show further down) with rules on which SSH key to pick per repo. I initially had a repo URL rewrite rule, but discarded that approach when I relealised I would need to update &lt;strong&gt;every single repo&lt;/strong&gt; I already had cloned locally. &lt;/p&gt;

&lt;p&gt;The decision tree then became the following for any work repo on GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git push    remote.origin.url = git@github.com:UiPath/some-repo.git
│
├─► ~/.gitconfig
│   └─ includeIf "hasconfig:remote.*.url:git@github.com:&amp;lt;arcane regex to match org name for UiPath&amp;gt;"
│      ──► pulls in ~/.gitconfig-github-uipath
│          (work email + work signing key)
│
├─► ~/.ssh/config
│   └─ Host github.com
│         IdentityFile ~/.ssh/id_ed25519.pub   (the work key)
│         IdentitiesOnly yes
│
└─► 1Password SSH agent
    ──► "the key matching this .pub, please" ──► Touch ID ──► 🎉
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll notice that there isn't a split in the &lt;code&gt;~/.ssh/config&lt;/code&gt; between my work and personal SSH keys. The decision on which key to use happens at commit (and signing) time based on the configs for the key and email inside each repo, which is managed with the different &lt;code&gt;.gitconfig*&lt;/code&gt; files. In the example above, the repo URL matches the catchall or default that I set to my personal GitHub key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Generate the keys
&lt;/h2&gt;

&lt;p&gt;I keep my SSH keys in &lt;a href="https://developer.1password.com/docs/ssh/" rel="noopener noreferrer"&gt;1Password&lt;/a&gt; rather than as files on disk, storing the work ones in my UiPath personal vault and any personal ones in my personal vault. The private key never leaves the vault, the agent prompts with Touch ID when something wants to use a key, and when I get the &lt;em&gt;next&lt;/em&gt; new laptop the keys are just... there. Generating is a case of &lt;strong&gt;New Item → SSH Key → Generate&lt;/strong&gt; in the app.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you aren't using 1Password, you can just remove the config lines that reference the agent in the code blocks below, and change the keys in the &lt;code&gt;~/.ssh/config&lt;/code&gt; to use the private keys by removing the &lt;code&gt;.pub&lt;/code&gt; from the end after generating them with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -C "my-work-email@example.com"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_personal -C "my-personal-email@example.com"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -C "my-work-email@example.com"&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are my keys on my work laptop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GitHub - work&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id_ed25519&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ed25519&lt;/td&gt;
&lt;td&gt;Filed under "default to use currently" in my brain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GitHub - personal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id_ed25519_personal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ed25519&lt;/td&gt;
&lt;td&gt;Same as above.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Azure DevOps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id_rsa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RSA 4096&lt;/td&gt;
&lt;td&gt;Azure DevOps &lt;a href="https://learn.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops" rel="noopener noreferrer"&gt;only supports RSA&lt;/a&gt;. Yes, in 2026.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on that RSA key, because the Microsoft docs send you down a rabbit hole: older versions told you to run &lt;code&gt;ssh-keygen -t rsa-sha2-256&lt;/code&gt;. That &lt;em&gt;works&lt;/em&gt;, but &lt;code&gt;rsa-sha2-256&lt;/code&gt; isn't a key type, it's a &lt;strong&gt;signature algorithm&lt;/strong&gt;. The key that comes out is a plain &lt;code&gt;ssh-rsa&lt;/code&gt; key, identical to what &lt;code&gt;ssh-keygen -t rsa -b 4096&lt;/code&gt; produces. What &lt;a href="https://learn.microsoft.com/en-us/lifecycle/announcements/sha-1-signed-content-retired" rel="noopener noreferrer"&gt;Azure DevOps actually deprecated in 2022&lt;/a&gt; was the old SHA-1 signature (&lt;code&gt;ssh-rsa&lt;/code&gt; as an &lt;em&gt;algorithm&lt;/em&gt;), and any OpenSSH from the last decade negotiates &lt;code&gt;rsa-sha2-512&lt;/code&gt; automatically. So: a normal RSA key, nothing special. If you find old forum threads saying "1Password's agent doesn't work with Azure DevOps", they're from before Microsoft added &lt;code&gt;rsa-sha2&lt;/code&gt; support. It works fine now.&lt;/p&gt;

&lt;p&gt;Next, download the &lt;strong&gt;public&lt;/strong&gt; half of each key from 1Password (the little dropdown on the "public key" field has a &lt;strong&gt;Download&lt;/strong&gt; option) and save them as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.ssh/id_ed25519.pub            # GitHub, work
~/.ssh/id_ed25519_personal.pub   # GitHub, personal
~/.ssh/id_rsa.pub                # Azure DevOps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These public keys are how we tell the agent &lt;em&gt;which&lt;/em&gt; of its keys to use for a given host. More on that in a second. You now need to upload the public parts of each keypair, and then also upload them separately for signing on GitHub only (Azure DevOps doesn't do commit signature verification at all as far as I can tell, so there's no signing entry to add.):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/settings/keys" rel="noopener noreferrer"&gt;Settings → SSH and GPG keys&lt;/a&gt;. Both the work and personal keys are added under &lt;em&gt;Authentication Key&lt;/em&gt; - I'm assuming you are following my pattern of having a distinct personal key when using your work computer. &lt;strong&gt;Both&lt;/strong&gt; Ed25519 keys go in as &lt;em&gt;Signing Keys&lt;/em&gt;, because GitHub only shows the &lt;strong&gt;Verified&lt;/strong&gt; badge when the signature matches a signing key on the account that owns the commit's email. Both emails need to be verified on the account too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure DevOps:&lt;/strong&gt; User settings → SSH public keys → New Key, and paste in the RSA one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Configure &lt;code&gt;ssh&lt;/code&gt; to use different keys
&lt;/h2&gt;

&lt;p&gt;Next we need to configure &lt;code&gt;ssh&lt;/code&gt; to know which key to use based on the URL of the repo we are dealing with with the following in &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; github.com
    &lt;span class="k"&gt;User&lt;/span&gt; git
    &lt;span class="k"&gt;IdentityAgent&lt;/span&gt; "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519.pub
    &lt;span class="k"&gt;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;

&lt;span class="k"&gt;Host&lt;/span&gt; ssh.dev.azure.com
    &lt;span class="k"&gt;User&lt;/span&gt; git
    &lt;span class="k"&gt;IdentityAgent&lt;/span&gt; "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_rsa.pub
    &lt;span class="k"&gt;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;

&lt;span class="c1"&gt;# Everything else still goes through the 1Password agent&lt;/span&gt;
&lt;span class="k"&gt;Host&lt;/span&gt; *
    &lt;span class="k"&gt;IdentityAgent&lt;/span&gt; "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important pair of lines in each block is &lt;code&gt;IdentityFile &amp;lt;something&amp;gt;.pub&lt;/code&gt; + &lt;code&gt;IdentitiesOnly yes&lt;/code&gt;. Without them, the agent offers &lt;em&gt;every&lt;/em&gt; key it has, one after the other, until the server accepts one. With three keys in the agent and the RSA one first, Azure DevOps is happy but GitHub gets offered all the keys available in &lt;code&gt;~/.ssh&lt;/code&gt; that will lead to &lt;code&gt;Too many authentication failures&lt;/code&gt; because the server gave up after six attempts. If you've never looked at what SSH does when it connects to a server, add in the very very very verbose parameter with &lt;code&gt;ssh -vvvv &amp;lt;rest&amp;gt;&lt;/code&gt; to see the details. Specifying &lt;code&gt;IdentitiesOnly yes&lt;/code&gt; says "only offer what I've listed", and pointing the &lt;code&gt;IdentityFile&lt;/code&gt; at a public key is the &lt;a href="https://developer.1password.com/docs/ssh/agent/advanced/#match-key-with-host" rel="noopener noreferrer"&gt;documented way&lt;/a&gt; to tell an agent &lt;em&gt;which&lt;/em&gt; private key you mean without having the private key on disk.&lt;/p&gt;

&lt;p&gt;There's no &lt;code&gt;HostkeyAlgorithms +ssh-rsa&lt;/code&gt; or &lt;code&gt;PubkeyAcceptedAlgorithms +ssh-rsa&lt;/code&gt; in the Azure DevOps block. If you have those lines from an older setup, &lt;a href="https://learn.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops#questions-and-troubleshooting" rel="noopener noreferrer"&gt;delete them&lt;/a&gt;; they re-enable the SHA-1 algorithm Microsoft is trying to get rid of.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt; the agent socket is &lt;code&gt;~/.1password/agent.sock&lt;/code&gt;. On macOS you can create a symlink with &lt;code&gt;ln -s ~/Library/Group\ Containers/2BUA8C4S2C.com.1password/t/agent.sock ~/.1password/agent.sock&lt;/code&gt; and use then use that instead of the full path. I prefer not to as I always forget to add it again when I move to a new laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; the 1Password agent takes over the &lt;code&gt;\\.\pipe\openssh-ssh-agent&lt;/code&gt; named pipe, so there is &lt;strong&gt;no &lt;code&gt;IdentityAgent&lt;/code&gt; line&lt;/strong&gt;, just drop it from every block. You do need to stop and disable the &lt;em&gt;OpenSSH Authentication Agent&lt;/em&gt; Windows service (1Password's &lt;a href="https://developer.1password.com/docs/ssh/get-started/#step-3-turn-on-the-1password-ssh-agent" rel="noopener noreferrer"&gt;setup guide&lt;/a&gt; walks through it), and tell Git for Windows to use the Windows OpenSSH client rather than its bundled one, because the bundled one can't see the pipe: &lt;code&gt;git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"&lt;/code&gt;. The config file lives at &lt;code&gt;C:\Users\&amp;lt;you&amp;gt;\.ssh\config&lt;/code&gt; with the same syntax.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Rules for &lt;code&gt;git&lt;/code&gt; on which keys to use when
&lt;/h2&gt;

&lt;p&gt;Now the part that took me the longest to get right. The global config holds the &lt;strong&gt;default&lt;/strong&gt; (work) identity, the signing setup, the credential helper for Azure DevOps over HTTPS, and the conditional includes. The rules specified with the &lt;code&gt;includIf&lt;/code&gt; lines point to specific &lt;code&gt;~/.gitconfig-&amp;lt;name&amp;gt;&lt;/code&gt; files to make it easier to manage if I need to add another set of rules in future. The main &lt;code&gt;~/.gitconfig&lt;/code&gt; file I have is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[user]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Cobus Bernard&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;cobus@work.example       # default identity: work&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;signingkey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssh-ed25519 AAAA...work   # the work *public* key, in full&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[gpg]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssh&lt;/span&gt;
&lt;span class="nn"&gt;[gpg "ssh"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;program&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/Applications/1Password.app/Contents/MacOS/op-ssh-sign&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;allowedSignersFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.ssh/allowed_signers&lt;/span&gt;
&lt;span class="nn"&gt;[commit]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;gpgsign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Azure DevOps over HTTPS uses Git Credential Manager instead of SSH
&lt;/span&gt;&lt;span class="nn"&gt;[credential]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;helper&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;helper&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/usr/local/share/gcm-core/git-credential-manager&lt;/span&gt;
&lt;span class="nn"&gt;[credential "https://dev.azure.com"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;useHttpPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;[credential "azrepos:org/uipath"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;cobus@work.example&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Azure DevOps repos, SSH and HTTPS forms
&lt;/span&gt;&lt;span class="nn"&gt;[includeIf "hasconfig:remote.*.url:git@ssh.dev.azure.com:v3/uipath/**"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.gitconfig-azure&lt;/span&gt;
&lt;span class="nn"&gt;[includeIf "hasconfig:remote.*.url:https://dev.azure.com/uipath/**"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.gitconfig-azure&lt;/span&gt;
&lt;span class="nn"&gt;[includeIf "hasconfig:remote.*.url:https://*@dev.azure.com/uipath/**"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.gitconfig-azure&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# GitHub: personal identity by default...
&lt;/span&gt;&lt;span class="nn"&gt;[includeIf "hasconfig:remote.*.url:git@github.com:*/**"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.gitconfig-github-personal&lt;/span&gt;
&lt;span class="nn"&gt;[includeIf "hasconfig:remote.*.url:https://github.com/*/**"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.gitconfig-github-personal&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# ...unless the org name starts with "uipath" (any casing).
# Must come AFTER the personal includes.
&lt;/span&gt;&lt;span class="nn"&gt;[includeIf "hasconfig:remote.*.url:git@github.com:[Uu][Ii][Pp][Aa][Tt][Hh]&lt;/span&gt;&lt;span class="na"&gt;*/**"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.gitconfig-github-uipath&lt;/span&gt;
&lt;span class="nn"&gt;[includeIf "hasconfig:remote.*.url:https://github.com/[Uu][Ii][Pp][Aa][Tt][Hh]&lt;/span&gt;&lt;span class="na"&gt;*/**"]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.gitconfig-github-uipath&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then have 3 named &lt;code&gt;.gitconfig&lt;/code&gt; files that contain the values to overwrite from the original one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;~/.gitconfig-github-personal&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[user]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;cobus@personal.example&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;signingkey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssh-ed25519 AAAA...personal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;~/.gitconfig-github-uipath&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[user]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;cobus@work.example&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;signingkey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssh-ed25519 AAAA...work&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;~/.gitconfig-azure&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[user]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;signingkey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssh-rsa AAAA...azure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Yes, signing Azure DevOps commits with the RSA key is pointless since Azure DevOps ignores signatures. It is there so that &lt;code&gt;git commit&lt;/code&gt; behaviour is the same for all repos and will open the 1Password dialog to allow access to the key.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Notes to future self
&lt;/h3&gt;

&lt;p&gt;I ran into these issues as I was setting this up and that's what made me decide to write it down so I can reference my TILs next time I make the same mistakes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;includeIf "hasconfig:remote.*.url:..."&lt;/code&gt;&lt;/strong&gt; (&lt;a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-codehasconfigremoteurlcode" rel="noopener noreferrer"&gt;Git 2.36+&lt;/a&gt;) conditionally includes a config file based on the remote URLs in the current repo. The better-known &lt;code&gt;includeIf "gitdir:..."&lt;/code&gt; version does the same based on &lt;em&gt;where the repo lives on disk&lt;/em&gt;, which means you have to be disciplined about &lt;code&gt;~/work/&lt;/code&gt; vs &lt;code&gt;~/personal/&lt;/code&gt; folders. In a next life I may be this organized, but today is not that day, so I went with the URL matching route to protect me from myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last include wins.&lt;/strong&gt; If two &lt;code&gt;includeIf&lt;/code&gt; blocks match the same repo, both files get included in order and the later one's &lt;code&gt;user.email&lt;/code&gt; overrides. That's why the &lt;code&gt;uipath&lt;/code&gt; block is last so that when a repo is under a UiPath org, the work config overrides the personal one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works during &lt;code&gt;clone&lt;/code&gt;, not just afterwards.&lt;/strong&gt; &lt;code&gt;git clone&lt;/code&gt; writes &lt;code&gt;remote.origin.url&lt;/code&gt; into the new repo's config before it fetches, and the include fires from then on. So &lt;code&gt;git clone git@github.com:UiPath/some-repo.git&lt;/code&gt; gets the work identity from the very first commit. No clone-then-fix-the-email dance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;While I didn't run into any issues with the repos I already had cloned locally, I did use &lt;code&gt;git config --show-origin user.signingkey&lt;/code&gt; in each of them as a sanity check before I made any new commits.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The HTTPS variants are not optional.&lt;/strong&gt; Azure DevOps hands out &lt;code&gt;https://dev.azure.com/uipath/...&lt;/code&gt; URLs by default, and some tooling adds your username in front of the host, so there are three patterns for one place. GitHub's HTTPS clone URL gets the same treatment.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Plural of "regex" is "regrets"
&lt;/h3&gt;

&lt;p&gt;To be clear, I haven't written regex by hand in a long time, originally you had to combine StackOverflow posts with online regex testers to fudge one that worked together. &lt;del&gt;My&lt;/del&gt; Claude's first version of the &lt;code&gt;uipath&lt;/code&gt; rule was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git@github.com:*[Uu][Ii][Pp][Aa][Tt][Hh]*/**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a leading &lt;code&gt;*&lt;/code&gt;, the patterns should stop at a &lt;code&gt;/&lt;/code&gt; the way it does in a shell. So "uipath anywhere in the org name" and a repo called &lt;code&gt;cobusbernard/uipath-scripts&lt;/code&gt; would correctly stay personal.&lt;/p&gt;

&lt;p&gt;Nope. So a few more tokens were burned to confirm why it wasn't working with a personal repo with &lt;code&gt;uipath&lt;/code&gt; in the name (I use this repo to share personal utility scripts to my work laptop):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git init &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd test&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git remote add origin git@github.com:cobusbernard/uipath-scripts.git
&lt;span class="nv"&gt;$ &lt;/span&gt;git config user.email
cobus@work.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;*&lt;/code&gt; happily matched &lt;code&gt;cobusbernard/&lt;/code&gt; and my personal repo used my work email and key. This is exactly what I was trying to avoid - as a rule I don't use my work laptop for personal work. This would have been a one-way pull from my personal repo to my work laptop, but I really don't want to accidentally commit to my personal repo with my work laptop.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-codehasconfigremoteurlcode" rel="noopener noreferrer"&gt;docs&lt;/a&gt; do say the pattern uses "standard globbing wildcards" plus &lt;code&gt;**&lt;/code&gt;, they just don't spell out that the standard &lt;code&gt;*&lt;/code&gt; here crosses &lt;code&gt;/&lt;/code&gt;. Dropping the leading &lt;code&gt;*&lt;/code&gt; so the pattern is anchored to the start of the org name fixes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git@github.com:[Uu][Ii][Pp][Aa][Tt][Hh]*/**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It matches &lt;code&gt;UiPath/&lt;/code&gt; and &lt;code&gt;uipath-labs/&lt;/code&gt; at the start as the org name, and leaves &lt;code&gt;cobusbernard/uipath-demo&lt;/code&gt; alone. The &lt;code&gt;[Uu][Ii]...&lt;/code&gt; character classes are there because the match is case-sensitive and GitHub happily hands out both &lt;code&gt;UiPath/&lt;/code&gt; and &lt;code&gt;uipath-labs/&lt;/code&gt; style URLs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fun fact: URLs are case sensitive for anything that isn't the domain or protocol (&lt;code&gt;https&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;ftp&lt;/code&gt;) -  see &lt;a href="https://www.rfc-editor.org/info/rfc3986/" rel="noopener noreferrer"&gt;RFC3986&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/WD-html40-970708/htmlweb.html" rel="noopener noreferrer"&gt;w3.org&lt;/a&gt;, so technically &lt;code&gt;https://github.com/uipath&lt;/code&gt; and &lt;code&gt;https://github.com/UiPath&lt;/code&gt; are 2 different URLs, but I haven't run into that with GitHub ever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4: Setting up commit signing
&lt;/h2&gt;

&lt;p&gt;Almost done now, the rest of this post is more informational notes to my future self. Most of the signing config is already in &lt;code&gt;~/.gitconfig&lt;/code&gt; above, the important lines are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gpg.format = ssh&lt;/code&gt; tells git to sign with an SSH key instead of GPG (&lt;a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat" rel="noopener noreferrer"&gt;Git 2.34+&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user.signingkey&lt;/code&gt; is the &lt;strong&gt;public key, in full&lt;/strong&gt;, not a path. Each include file sets its own, so personal repos sign with the personal key, UiPath repos sign with the work key, and Azure DevOps repos sign with the RSA key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gpg.ssh.program = op-ssh-sign&lt;/code&gt; hands the actual signing to 1Password, which means a Touch ID prompt on &lt;code&gt;git commit&lt;/code&gt;. The private key stays in the vault.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commit.gpgsign = true&lt;/code&gt; so I don't have to remember &lt;code&gt;-S&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To verify signatures locally (&lt;code&gt;git log --show-signature&lt;/code&gt;), git also needs an allowed-signers file mapping emails to public keys, which is what &lt;code&gt;gpg.ssh.allowedSignersFile&lt;/code&gt; points at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ~/.ssh/allowed_signers&lt;/span&gt;
&lt;span class="k"&gt;cobus&lt;/span&gt;@work.example ssh-ed25519 AAAA...work
&lt;span class="k"&gt;cobus&lt;/span&gt;@personal.example ssh-ed25519 AAAA...personal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without it, &lt;code&gt;git log --show-signature&lt;/code&gt; complains that &lt;code&gt;gpg.ssh.allowedSignersFile needs to be configured&lt;/code&gt;. It doesn't block anything, it's just another extra warning each time I did a commit.&lt;/p&gt;

&lt;p&gt;GitHub shows a commit as &lt;strong&gt;Verified&lt;/strong&gt; when the signature matches a &lt;em&gt;signing key&lt;/em&gt; registered on the account that owns the commit's &lt;strong&gt;email address&lt;/strong&gt;. That's the whole reason email and signing key live together in the include files: if the email says personal but the signature is from the work key, GitHub shrugs and shows &lt;em&gt;Unverified&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Linux/Windows signing program:&lt;/strong&gt; &lt;code&gt;/opt/1Password/op-ssh-sign&lt;/code&gt; on Linux, and &lt;code&gt;C:/Users/&amp;lt;you&amp;gt;/AppData/Local/1Password/app/8/op-ssh-sign.exe&lt;/code&gt; on Windows (forward slashes are fine in &lt;code&gt;.gitconfig&lt;/code&gt;). Or let 1Password write it for you: open the SSH key item, &lt;strong&gt;⋯ → Configure Commit Signing → Edit Automatically&lt;/strong&gt;. Just move the &lt;code&gt;signingkey&lt;/code&gt; line it writes into the right include file afterwards, because it writes to the global &lt;code&gt;[user]&lt;/code&gt; section and will stomp on the default when you run it for the second key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Does It Actually Work?
&lt;/h2&gt;

&lt;p&gt;Usually you would test access with &lt;code&gt;ssh -T git@github.com&lt;/code&gt; and it will return &lt;code&gt;Hi cobusbernard!&lt;/code&gt; . This doesn't tell me if the setup is correct, as any SSH key I have set up on my GitHub account will be successful. On ADO, it looks like it failed, but this is the success message, if the key was invalid, you would get a "permissions denied" error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@ssh.dev.azure.com
remote: Shell access is not supported.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To properly test, you need to run the following inside the each repo you have cloned locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git config &lt;span class="nt"&gt;--show-origin&lt;/span&gt; user.email
file:/Users/cobus/.gitconfig-github-personal    cobus@personal.example

&lt;span class="nv"&gt;$ &lt;/span&gt;git config &lt;span class="nt"&gt;--show-origin&lt;/span&gt; user.signingkey
file:/Users/cobus/.gitconfig-github-personal    ssh-ed25519 AAAA...personal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--show-origin&lt;/code&gt; flag will print out which file the config value displayed comes from. If you are still having issues, it is time to dive into the detailed &lt;code&gt;ssh&lt;/code&gt; logs to see which key was sent. Remember the part earlier where I used &lt;code&gt;IdentitiesOnly yes&lt;/code&gt; to ensure only 1 specific key would be sent? I used the following to troubleshoot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GIT_SSH_COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ssh -v"&lt;/span&gt; git fetch 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'Offering|Server accepts|Authenticated'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see only 1 line for &lt;code&gt;Offering public key&lt;/code&gt;, and it should match the  &lt;code&gt;.pub&lt;/code&gt; you specified in your configs, otherwise there is a mistake somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honourable Mentions
&lt;/h2&gt;

&lt;p&gt;These are some more notes for future-Cobus that caused present-Cobus some confusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;includeIf hasconfig&lt;/code&gt; only fires inside a repo.&lt;/strong&gt; &lt;code&gt;git ls-remote git@github.com:UiPath/x.git&lt;/code&gt; run from your home directory has no remotes to match, so it uses the default. In practice this never matters, but it confused me for ten minutes when I was testing my setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;ssh://&lt;/code&gt; URL form is a different prefix.&lt;/strong&gt; GitHub's UI gives you &lt;code&gt;git@github.com:org/repo.git&lt;/code&gt;, which is what all the patterns above use. If you also use &lt;code&gt;ssh://git@github.com/org/repo.git&lt;/code&gt; style URLs, you need to add a matching &lt;code&gt;includeIf&lt;/code&gt; with &lt;code&gt;ssh://git@github.com/**&lt;/code&gt;. I don't currently have this issue, but adding so I can find this post when I do need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your git version.&lt;/strong&gt; &lt;code&gt;hasconfig&lt;/code&gt; needs 2.36 (2022), SSH signing needs 2.34. Apple's bundled git is fine on anything recent; &lt;code&gt;brew install git&lt;/code&gt; if not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The empty &lt;code&gt;helper =&lt;/code&gt; line is on purpose.&lt;/strong&gt; Git appends credential helpers rather than replacing them, so an empty value first clears whatever the system &lt;code&gt;gitconfig&lt;/code&gt; set, and then 2nd line sets the only one I wanted to use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping it up
&lt;/h2&gt;

&lt;p&gt;While I had to deal with this a long time ago and knew the rough outlines of how to handle it, it did seem overly complex when I started redoing it. Any time you do something for the first time, this is the case until you understand it, then it almost feels like it is simple. In the end I had to only edit &lt;code&gt;~/.ssh/config&lt;/code&gt; and &lt;code&gt;~/.gitconfig&lt;/code&gt;, and then add the 3 extra &lt;code&gt;~/.gitconfig-&amp;lt;name&amp;gt;&lt;/code&gt; files to make it nice and clean.&lt;/p&gt;

&lt;p&gt;This is the setup I ended with after going back and forth with Claude, if you've got a cleaner way to do the case-insensitive org match, or you've found a way to make Azure DevOps accept an Ed25519 key, I'd love to hear it. Let me know in the comments below!&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Everything Is an Agent Now, and they are arguing with me</title>
      <dc:creator>Cobus Bernard 🌩🥑🇿🇦 in 🇺🇸</dc:creator>
      <pubDate>Tue, 18 Aug 2026 19:58:34 +0000</pubDate>
      <link>https://dev.to/uipath/everything-is-an-agent-now-and-they-are-arguing-with-me-5fpa</link>
      <guid>https://dev.to/uipath/everything-is-an-agent-now-and-they-are-arguing-with-me-5fpa</guid>
      <description>&lt;p&gt;I joined UiPath in July 2026 after 7+ years as a Developer Advocate at AWS. Just over a month in, and I've had to unlearn something I've depended on when building automations most of my career: that some of my steps in my code now have &lt;strong&gt;&lt;em&gt;opinions&lt;/em&gt;&lt;/strong&gt;. This is where &lt;em&gt;non-deterministic&lt;/em&gt; has been creeping into the way I build things.&lt;/p&gt;

&lt;p&gt;First, a confession: I spent the better part of a decade building systems whose entire appeal was that they did &lt;em&gt;exactly&lt;/em&gt; what I told them to. &lt;a href="https://github.com/cobusbernard/hashitalks-africa-demo/blob/master/environments/Makefile" rel="noopener noreferrer"&gt;Terraform plans&lt;/a&gt;. &lt;a href="https://builder.aws.com/content/2hsUp8ZV7UoQpVCApnaTOBljCai/building-a-serverless-web-scraper-with-a-little-lot-of-help-from-amazon-q-part-1" rel="noopener noreferrer"&gt;Lambda functions&lt;/a&gt;. Pipelines that either went green or red and produced the same output every single time given the same input. Determinism was the whole product. &lt;em&gt;Enter stage-left: non-determinism.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; the word "agent" has been stretched so thin it's basically transparent, and the most useful thing I've learnt isn't a product, it's a line: &lt;em&gt;Work is now split into steps for known work (deterministic) and ones that benefit from judgement (non-deterministic)&lt;/em&gt;. Almost everything in the UiPath agentic stack makes sense the moment you know which side of that line a given piece is standing on. These are my notes from figuring that out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The line
&lt;/h2&gt;

&lt;p&gt;Here's the trap I walked straight into: I assumed "agentic" meant &lt;em&gt;"put an LLM in the middle and let it sort things out"&lt;/em&gt;. So my first instinct was to reach for an agent for everything, including the parts that were an &lt;code&gt;if&lt;/code&gt; statement wearing a very expensive hat.&lt;/p&gt;

&lt;p&gt;Non-determinism is a cost, not a feature. You pay for it in latency, in tokens, and in the thing that actually matters at enterprise scale: &lt;em&gt;explainability&lt;/em&gt;. When the finance team asks &lt;em&gt;"why invoice #4471 got rejected"&lt;/em&gt;, &lt;del&gt;"it worked on my computer"&lt;/del&gt; &lt;em&gt;"the model felt strongly about it"&lt;/em&gt; is not an answer anyone accepts. So the question for every step in a process becomes: &lt;strong&gt;does this step genuinely need judgement, or do I just not want to write the rules down?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This the the decision tree I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Does this step need judgement?
                                   │
                 ┌─────────────────┴─────────────────┐
                NO                                  YES
                 │                                   │
         Is there an API?                    Can it be wrong?
                 │                                   │
        ┌────────┴────────┐                 ┌────────┴─────────┐
       YES               NO            low stakes         high stakes
        │                 │                 │                  │
 Coded Function    RPA process        Coded Agent      Coded Agent
 Python, typed,    .xaml - drives     LLM + tools,     + human approval
 no LLM            the UI of that     fully traced     (Action Center)
                   1997 ERP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you noticed the right-most branch for &lt;em&gt;"Can it be wrong?"&lt;/em&gt; doesn't have the usual "Yes" / "No" branching, &lt;strong&gt;there is no option for "No"&lt;/strong&gt;, LLMs will always have a non-zero change of getting it wrong, this is the &lt;em&gt;non-deterministic&lt;/em&gt; nature again. I keep coming back to this: "Agentic" in an enterprise doesn't mean "no humans." It means the human moves from &lt;em&gt;doing the work&lt;/em&gt; to &lt;em&gt;approving the interesting 4% of it where an incorrect decision has a high stakes impact&lt;/em&gt;, and the platform's job is to make that handoff a first-class node rather than a Slack message someone forgets to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map
&lt;/h2&gt;

&lt;p&gt;Now that we've covered the mental model, here's roughly how I've mapped out the different UiPath products. This is my mental version, not an org chart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────┐
│ Studio Web · Agent Builder · your own editor + the uip CLI │ build
├────────────────────────────────────────────────────────────┤
│                                                            │
│                   M A E S T R O   F L O W                  │ orchestrate
│       the conductor - what runs, when, in what order,      │
│             and what happens when it goes wrong            │
│                                                            │
│ ┌───────────┐  ┌────────────┐  ┌──────────┐  ┌───────────┐ │
│ │   Coded   │  │   Coded    │  │  Human   │  │    RPA    │ │ do the 
│ │  Agents   │  │ Functions  │  │ approval │  │ processes │ │ work
│ │   (LLM)   │  │  (no LLM)  │  │  (HITL)  │  │  (.xaml)  │ │
│ └───────────┘  └────────────┘  └──────────┘  └───────────┘ │
│                                                            │
├────────────────────────────────────────────────────────────┤
│ Orchestrator · Integration Service · Data Fabric · IXP     │ ground 
│ jobs, queues, assets · SaaS connectors · entities · docs   │ floor
├────────────────────────────────────────────────────────────┤
│ AI Trust Layer + Governance - policies, guardrails, audit  │ the adults
└────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part that surprised me is the bottom two layers. I came in expecting the agent frameworks to be the interesting bit, and they're genuinely good, but they're also the bit anyone can &lt;code&gt;pip install&lt;/code&gt;. What you can't &lt;code&gt;pip install&lt;/code&gt; is twenty years of &lt;em&gt;"this &lt;a href="https://docs.uipath.com/integration-service/automation-cloud/latest/user-guide/introduction?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;connector&lt;/a&gt; handles the fact that integration Z's session tokens expire weirdly on Tuesdays"&lt;/em&gt;. The ground floor is the moat, and the agentic layer is what finally makes it addressable by something other than a human clicking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coded Functions: the code you mostly know
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://docs.uipath.com/functions/automation-cloud/latest/user-guide/about-functions?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;Coded Function&lt;/a&gt; is a deterministic unit of Python. Typed input, typed output, no LLM, no agent loop. You scaffold it with &lt;code&gt;uip functions new &amp;lt;name&amp;gt; -l py&lt;/code&gt;, and it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uipath.tracing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traced&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;      &lt;span class="c1"&gt;# populated on failure, empty on success
&lt;/span&gt;    &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

&lt;span class="nd"&gt;@traced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validate_invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uipath&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matches_purchase_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAILED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice, because I used them incorrectly on my first attempts. First, &lt;strong&gt;errors are returned, not raised&lt;/strong&gt;, you populate those &lt;code&gt;error_*&lt;/code&gt; fields rather than letting the exception escape, because the caller might be a flow that needs to route on the failure rather than just die. Second, the &lt;code&gt;@traced&lt;/code&gt; decorator: it makes the function show up in the platform's trace view alongside the LLM calls.&lt;/p&gt;

&lt;p&gt;Similar to how you specify function name as the entrypoint for a Lambda function, set the filename + function name in &lt;code&gt;uipath.json&lt;/code&gt;. This is used when deploying the solution to indicate it is a Function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"functions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main.py:validate_invoice"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is so special about a function: a Python Coded Function gets &lt;strong&gt;real job semantics&lt;/strong&gt;: an &lt;a href="https://docs.uipath.com/orchestrator/automation-cloud/latest/user-guide/introduction?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;Orchestrator&lt;/a&gt; job ID, retries, scheduling, an audit trail. It's not a helper buried inside an agent's context window where nobody can see it fail. It's a first-class citizen that a flow, an agent, a BPMN process, or a plain API call can all invoke. Every time I've been tempted to give an agent "just one more tool," the better answer has usually been a function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coded Agents: your framework, someone else's ops problem
&lt;/h2&gt;

&lt;p&gt;When the step &lt;em&gt;does&lt;/em&gt; need judgement, you write an actual &lt;a href="https://docs.uipath.com/agents/automation-cloud/latest/user-guide/about-coded-agents?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;coded agent&lt;/a&gt; in Python with the &lt;a href="https://docs.uipath.com/sdk/other/latest/developer-guide/using-agents-sdks?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;framework&lt;/a&gt; you'd have picked anyway: LangGraph, LlamaIndex, or OpenAI Agents. There's no proprietary DSL to learn. Your LangGraph graph is a LangGraph graph.&lt;/p&gt;

&lt;p&gt;The interesting part is what your agent reaches for at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              your Python agent (e.g. a LangGraph graph)
                              │
       ┌──────────────┬───────┴────────┬────────────────┐
       ▼              ▼                ▼                ▼
  LLM Gateway    Coded Function   IS connector     Context
  model access,  deterministic    Salesforce,      Grounding
  keys, policy,  helper you       NetSuite, SAP,   your indexed
  BYO models     already trust    Workday, …       documents
       │              │                │                │
       └──────────────┴───────┬────────┴────────────────┘
                              ▼
            every hop traced, every call governed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You never put a model key in your code, you go through the &lt;a href="https://docs.uipath.com/automation-cloud/automation-cloud/latest/admin-guide/about-ai-trust-layer?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;gateway&lt;/a&gt;, which is also where BYO-model configs and policies live. Tools come from the same catalogue the rest of the platform uses, so &lt;code&gt;create_process_tool&lt;/code&gt; lets an agent kick off an existing RPA process, and &lt;code&gt;create_escalation_tool&lt;/code&gt; lets it hand a decision to a human in &lt;a href="https://docs.uipath.com/action-center/automation-cloud/latest/user-guide/introduction?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;Action Center&lt;/a&gt; without you hand-rolling a task queue.&lt;/p&gt;

&lt;p&gt;And then there's the part that made me genuinely happy, because it's the part everyone skips: &lt;code&gt;uip codedagent eval&lt;/code&gt; runs your agent against an eval set with real evaluators before you ship it. The workflow assumes you'll write a smoke set. Tests for the non-deterministic parts, treated as table stakes rather than a blog post someone means to write. Coming from infrastructure, where "did the plan apply cleanly" was the whole test suite, I find this a healthy amount of paranoia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flow: where it stops being a demo
&lt;/h2&gt;

&lt;p&gt;An agent on its own is a demo. A &lt;em&gt;process&lt;/em&gt; is a sequence of steps with branching, retries, humans, deadlines, and a nasty legacy system at the end of it. That's &lt;a href="https://docs.uipath.com/maestro/automation-cloud/latest/user-guide/overview?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;Maestro Flow&lt;/a&gt;: a &lt;code&gt;.flow&lt;/code&gt; project (JSON on disk, visual in &lt;a href="https://docs.uipath.com/studio-web/automation-cloud/latest/user-guide/overview?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;Studio Web&lt;/a&gt;, either way it's a real artifact you can diff) that wires everything above into something a business will actually run on a Tuesday.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌─────────┐
  │ Trigger │  new mail lands in invoices@
  └────┬────┘
       ▼
  ┌──────────────────┐
  │ IXP extraction   │  pull the fields out of the attached PDF
  └────┬─────────────┘
       ▼
  ┌──────────────────┐
  │ Coded Function   │  validate + normalise
  │ validate_invoice │  no LLM - same input, same answer, always
  └────┬─────────────┘
       ▼
       ◆ amount &amp;gt; 10k or PO mismatch?
       │
   yes │                          no
  ┌────▼───────────┐               │
  │ Human approval │               │
  │ (Action Center)│               │
  └────┬───────────┘               │
       └──────────┬────────────────┘
                  ▼
  ┌──────────────────────────────────────┐
  │ Coded Agent: reconcile_discrepancies │
  │ LangGraph - reasons about the messy  │
  │ cases, escalates when unsure         │
  └────┬─────────────────────────────────┘
       ▼
  ┌──────────────────┐
  │ RPA process      │  keys it into the ERP that has no API
  │  (.xaml)         │  and, let's be honest, never will
  └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the mix in that diagram: exactly one node has an LLM in it. That's not me being conservative, that's the shape most real processes take. The agent earns its place on the messy branch, and everything else stays boring on purpose.&lt;/p&gt;

&lt;p&gt;The bit that isn't visible in a diagram is what happens when it breaks. A running flow is an &lt;em&gt;instance&lt;/em&gt; you can pause, resume, cancel or retry. Failures land as incidents with traces attached. The whole thing packages into a &lt;a href="https://docs.uipath.com/solutions-management/automation-suite/2.2510/user-guide/solution-components?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;solution&lt;/a&gt; (&lt;code&gt;.uipx&lt;/code&gt;) so the flow, its functions, its agents and their resource bindings are versioned and deployed together, instead of the alternative I know far too well, which is six repos, a Confluence page, and &lt;a href="https://www.hopedrivendevelopment.com/" rel="noopener noreferrer"&gt;hope driven development&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm poking at next
&lt;/h2&gt;

&lt;p&gt;I'm just over one month in, so treat all of the above as field notes rather than doctrine, I'm still finding the sharp edges (and cheerfully filing feedback about them). Next on my list: pushing much harder on evals, because "how do you know your agent still works after you changed the prompt" is the question I think the whole industry is currently &lt;del&gt;ignoring&lt;/del&gt; getting to, and &lt;a href="https://docs.uipath.com/agents/automation-cloud/latest/user-guide/mcp-servers?utm_source=devto&amp;amp;utm_medium=organic_social&amp;amp;utm_campaign=awareness&amp;amp;utm_team=devrel&amp;amp;utm_team_geo=global&amp;amp;utm_term=cb_20260818" rel="noopener noreferrer"&gt;MCP servers&lt;/a&gt; via AgentHub, where the tool-catalogue story gets a lot more interesting.&lt;/p&gt;

&lt;p&gt;If you're a cloud/DevOps person eyeing this space and wondering whether the "agentic" hype is real: it is, but not in the way the marketing implies. It's not that everything becomes an agent. It's that you finally get to put judgement &lt;em&gt;in&lt;/em&gt; a process without giving up the determinism of everything around it, and the interesting engineering is in drawing that line well.&lt;/p&gt;

&lt;p&gt;Come tell me where I've got it wrong. I'll be in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>llm</category>
    </item>
    <item>
      <title>Robotic Process Automation (RPA)</title>
      <dc:creator>Truong</dc:creator>
      <pubDate>Sat, 07 Nov 2020 13:57:58 +0000</pubDate>
      <link>https://dev.to/uipath/robotic-process-automation-rpa-29dn</link>
      <guid>https://dev.to/uipath/robotic-process-automation-rpa-29dn</guid>
      <description>&lt;p&gt;&lt;a href="https://www.uipath.com/" rel="noopener noreferrer"&gt;UiPath&lt;/a&gt; - It can handle complex processes. This tool is ideal for businesses of all sizes.&lt;br&gt;
UiPath pros:&lt;br&gt;
-no programming skills required;&lt;br&gt;
-easy to use with drag and drop capability;&lt;br&gt;
-it provides useful features for free.&lt;br&gt;
-Large marketplace applications&lt;br&gt;
UiPath minus:&lt;br&gt;
-limited coding functionality.&lt;br&gt;
I can personally suggest you to go for &lt;a href="https://academy.uipath.com/" rel="noopener noreferrer"&gt;UiPath Platform course&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>uipath</category>
      <category>rpa</category>
      <category>roboticprocessautomation</category>
    </item>
  </channel>
</rss>
