Skip to content
CloudOps
Newsletter
All guides
AI for Microsoft Teams By James Joyner IV · · 11 min read

Build a Custom Connector for Power Automate to Reach Internal APIs

Out-of-box connectors can't reach your internal DevOps APIs. A custom connector wraps your OpenAPI spec so Teams flows can call it. Here's the build, secured.

  • #microsoft-teams
  • #power-automate
  • #custom-connector
  • #openapi
  • #integration

The request landed in our team channel on a Tuesday: “Can someone make it so a Teams approval kicks off a deploy?” Simple enough on the surface, except the deploy lives behind an internal API on a private VLAN that the Power Automate runners have never heard of, and never will without help. The out-of-box connectors are wonderful for SharePoint, Outlook, and a few hundred SaaS products, but none of them know how to reach https://deploybot.internal.corp/v1/deploy. That gap is exactly what a custom connector exists to fill: a thin, declarative wrapper around your own API that makes it look, to a flow author, like any other connector in the gallery.

I have built a handful of these now, and the pattern has settled into something repeatable. This post is the build I wish I’d had in front of me the first time, plus the security guardrails I bolted on after the first one made me nervous.

What a custom connector actually is

A custom connector is not code you host. It is metadata that the Power Platform stores and interprets. At runtime, when a flow calls your connector’s action, the platform reads that metadata and makes the HTTP call on your behalf. The whole thing reduces to two files:

  • apiDefinition.swagger.json — an OpenAPI 2.0 (Swagger) document describing your host, paths, operations, parameters, and responses. Power Platform is still firmly on the 2.0 schema, not 3.0, so don’t paste a modern OpenAPI 3.1 spec and expect it to import.
  • apiProperties.json — the platform-specific layer: authentication configuration, connection parameters, and any policy templates.

In the maker portal the connector is split into tabs that map cleanly onto these files: General (the host and base URL, icon, description), Security (auth type and its settings), Definition (your actions and triggers), and Code (an optional C# script transform, capped at 1 MB and a 5-second execution budget). Ninety percent of connectors never touch the Code tab. Get comfortable thinking of the connector as “an OpenAPI spec plus a security stanza” and the rest follows.

Drafting the spec — let AI do the typing

Hand-writing Swagger 2.0 by hand is tedious and error-prone, and this is precisely the kind of structured, well-documented task where an AI assistant earns its keep. I treat Claude or GitHub Copilot the way I’d treat a fast junior engineer: I hand it the API’s existing route documentation and ask it to draft the apiDefinition.swagger.json, then I review every line before anything touches a tenant. The model is genuinely good at the mechanical shape — paths, parameter types, response schemas — and it never gets bored on the fortieth field.

What it does not get to do is see anything real. Never paste a live tenant credential, client secret, or API key into a prompt. I describe the auth scheme in the abstract (“API key in an X-API-Key header”) and wire the actual secret in later, by hand, inside the secured connector. The AI drafts the structure; I own the secrets and the final sign-off.

Here’s a trimmed action — a single POST that triggers a deployment:

{
  "swagger": "2.0",
  "info": { "title": "Internal Deploy API", "version": "1.0" },
  "host": "deploybot.internal.corp",
  "basePath": "/v1",
  "schemes": ["https"],
  "paths": {
    "/deploy": {
      "post": {
        "summary": "Trigger a deployment",
        "operationId": "TriggerDeploy",
        "consumes": ["application/json"],
        "parameters": [
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "type": "object",
              "properties": {
                "service": { "type": "string" },
                "environment": { "type": "string", "enum": ["staging", "prod"] }
              },
              "required": ["service", "environment"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deployment accepted",
            "schema": {
              "type": "object",
              "properties": { "deployId": { "type": "string" } }
            }
          }
        }
      }
    }
  }
}

A clean operationId, a typed body, and an enum on environment so the flow author gets a dropdown instead of a free-text box. Small details like that enum are what separate a connector people enjoy using from one they fight.

Pro Tip: Give every operation a stable, human-readable operationId. The Power Automate UI uses it as the action’s display name, and changing it later silently breaks every existing flow that referenced the old name.

Authentication — pick the weakest thing that still works

The Security tab offers four options, and the right one is the one that grants the least privilege your API can tolerate:

  • No authentication — only ever for genuinely public read-only endpoints. I have never shipped this.
  • API Key — a header or query parameter the connection user supplies once. Simple, and fine for internal service-to-service calls where the key maps to a scoped service account.
  • Basic — username and password per connection. Avoid unless the API genuinely offers nothing else.
  • OAuth 2.0 — the right answer when the API supports it. You configure the auth/token URLs, client ID, client secret, and scopes. This is where least-privilege lives: request only the scopes the flow needs, never a blanket api://.../.default if a narrower scope exists.

The auth config is mirrored in apiProperties.json. Here’s the security stanza for the API-key version above:

{
  "properties": {
    "connectionParameters": {
      "api_key": {
        "type": "securestring",
        "uiDefinition": {
          "displayName": "API Key",
          "description": "The X-API-Key for the internal deploy service",
          "tooltip": "Provide your scoped service-account key",
          "constraints": { "clear": false, "required": "true" }
        }
      }
    },
    "iconBrandColor": "#0078d4",
    "capabilities": [],
    "policyTemplateInstances": []
  }
}

Note the securestring type — that ensures the key is masked in the UI and stored encrypted, not echoed back. When I review an AI-drafted apiProperties.json, this is the first thing I check, because a model will occasionally draft a plain string and quietly leak the field into logs.

Injecting the header with a policy template

The API-key parameter above captures the key, but something has to put it on the wire. Rather than write C# in the Code tab, use a policy templatesetHeader injects the connection’s key value into every request:

"policyTemplateInstances": [
  {
    "templateId": "setheader",
    "title": "Inject API key header",
    "parameters": {
      "x-ms-apimTemplateParameter.name": "X-API-Key",
      "x-ms-apimTemplateParameter.value": "@connectionParameters('api_key')",
      "x-ms-apimTemplate-policySection": "Request",
      "x-ms-apimTemplate-operationName": []
    }
  }
}

Policy templates run in the platform’s gateway layer, so the secret never appears in the connector definition itself — it’s resolved from the encrypted connection at call time. There are templates for setting and removing headers, string conversion of query params, and routing; setHeader covers the overwhelming majority of internal-API needs.

Reaching the internal network — the on-premises data gateway

Here is the part everyone forgets until their first 502. Power Automate’s runners live in Microsoft’s cloud. Your internal API lives behind a firewall they cannot route to. The bridge is the on-premises data gateway: a small agent you install on a machine inside your network that maintains an outbound connection to Azure and proxies requests inward.

For a custom connector, you flip on “Connect via on-premises data gateway” on the General tab. When a flow author creates a connection, they then select which installed gateway to use, and the platform tunnels the HTTP call through it to deploybot.internal.corp. No inbound firewall rule, no public exposure of the API — the gateway dials out, never in. For internal DevOps APIs this is almost always the deciding architectural choice, and it’s worth standing up the gateway on a hardened, monitored host because it now sits on the path to your deploy plane.

Webhook triggers, not just actions

Actions are pull. Sometimes you want push: the API tells the flow something happened. Custom connectors support webhook triggers for exactly this. You define an operation with "x-ms-trigger": "single" and a pair of webhook subscription endpoints — one the platform calls to register a callback URL when a flow turns on, and one to unregister when it turns off:

"x-ms-trigger": "single",
"x-ms-notification-content": {
  "schema": { "$ref": "#/definitions/DeployFinishedEvent" }
}

When you publish the flow, Power Automate calls your subscribe endpoint, hands it a generated callback URL, and your API POSTs to that URL whenever the event fires. Your deploy service can now notify a Teams channel the instant a rollout finishes — no polling. The catch is lifecycle: your API is responsible for cleaning up subscriptions when the unsubscribe endpoint is called, or you’ll leak dead callback URLs.

Source control and ALM — get it out of the portal

Clicking through the maker portal is fine for the first draft and a disaster for the tenth change. Both connector files belong in Git, and two CLIs get them there:

  • paconn — the Python paconn package. paconn download pulls apiDefinition.swagger.json and apiProperties.json for an existing connector; paconn update pushes them back. This is my go-to for round-tripping a connector into a repo.
  • pac connector (Power Platform CLI)pac connector create, pac connector update, and pac connector download do the equivalent and integrate with the broader pac tooling you may already use for solutions and pipelines.

Once the JSON is in source control, the AI assistant becomes useful for the maintenance loop too — diffing a proposed swagger change, spotting a missing required flag, drafting the changelog. I’ll often run the diff past Copilot in the editor before I commit, but the merge decision stays human.

For real ALM, package the connector inside a solution. A custom connector is a first-class solution component, so adding it to an unmanaged solution lets you export it and import a managed version into test and production environments. Pair that with environment variables for the host and base URL, and the same connector definition promotes cleanly from dev to prod without hand-editing deploybot.internal.corp into deploybot.staging.corp in three places.

Pro Tip: Put the connector’s base URL and host in solution environment variables, not hard-coded in the swagger. Promotion across environments then becomes a config change, not a definition edit — and you stop accidentally pointing a prod flow at a staging API.

The review gate that keeps you out of trouble

I run every connector through the same human checklist before it touches the production tenant, regardless of how clean the AI draft looked: Is the auth type the least-privileged option the API supports? Are OAuth scopes scoped, not .default? Is every secret field a securestring? Is the gateway host hardened and monitored? Does the webhook unsubscribe path actually delete subscriptions? The model drafts fast and accurately, but it has no stake in your blast radius — that’s the human’s job. If you want a repeatable prompt for that audit, I keep mine in the prompt workspace, and there are starter connector-review prompts over in the prompts library and the curated prompt packs.

Wrapping up

A custom connector is deceptively small — two JSON files and a checkbox for the gateway — but it’s the seam where your internal infrastructure meets the low-code world your colleagues already live in. Let the AI draft the spec and the config; it’s genuinely fast and tidy at it. Then do the part the model can’t: verify the security, scope the permissions, keep the real credentials in your own hands, and promote it through a solution like the production artifact it now is. For more on wiring DevOps automation into Teams, browse the rest of the Microsoft Teams category.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.