Skip to content

amacey-msft/copilot-studio-servicenow-bridge

Repository files navigation

Copilot Studio ↔ ServiceNow Live Agent Bridge

A reference implementation that lets a Microsoft Copilot Studio webchat hand a conversation off to a real ServiceNow Agent Workspace chat session — bidirectionally, with no page refresh and no context switch for the user.

Browser webchat ──► Copilot Studio ──► (handoff) ──► Flask bridge ──► ServiceNow
                                                                         │
                Browser webchat ◄──── (live messages both ways) ─────────┘
                               (Service Now Agent Workspace pane)

End-to-end flow

This repo packages the result of figuring out — the hard way — exactly which ServiceNow tables, APIs, and configuration steps are required to make Advanced Work Assignment (AWA) actually route a chat that originated outside ServiceNow, and how to push messages into a live chat so the agent's pane updates instantly.

What's in here

Path Purpose
docs/01-architecture.md Architecture, data flow, why each piece exists.
docs/02-servicenow-setup.md Step-by-step ServiceNow web UI setup. The big one.
docs/03-bridge-backend.md Flask bridge: env vars, endpoints, deployment.
docs/04-copilot-studio.md Two CS agents (web=awm_contosoithelp unauth, Teams=crd20_itHelpDeskTriageAssistant Entra Agent ID) and the shared A2A Connected Agent.
docs/05-browser-webchat.md Browser-side state machine and reference snippets.
docs/06-end-to-end-test.md Verification probe and what success looks like.
docs/07-troubleshooting.md Symptom → cause → fix table built from the dead ends.
docs/08-api-reference.md Every ServiceNow API and table touched, with rationale.
docs/09-production-hardening.md Checklist before promoting beyond a dev PDI.
docs/10-teams-channel-overview.md Microsoft Teams channel architecture and design.
docs/14-teams-a2a-setup.md Teams via Copilot Studio A2A “Add an agent” connector (teams_a2a/).
servicenow/ Three Scripted REST scripts to paste into ServiceNow.
bridge/ Reference Flask bridge (servicenow_bridge.py).
web/ Reference HTML page with the bot ↔ live agent state machine.
teams_a2a/ Teams relay registered with Copilot Studio as an A2A agent (request/response + proactive push).

Quick start

  1. Read docs/01-architecture.md (5 min).
  2. Work through docs/02-servicenow-setup.md in your dev PDI. End state: a POST to the scripted REST /open_chat endpoint returns a routed IMS####### and your test agent gets a chat invite in their Inbox.
  3. Run the bridge (docs/03-bridge-backend.md) locally, exposed via a tunnel.
  4. Configure the two Copilot Studio agents and register teams_a2a as a Connected Agent on each (docs/04-copilot-studio.md).
  5. Run the end-to-end probe in docs/06-end-to-end-test.md.

Important: the bridge must be reachable from ServiceNow and from teams_a2a

ServiceNow's outbound Business Rule POSTs the bridge directly. The CS Connected Agent (teams_a2a, hosted as ca-cps-sn-skill on ACA) calls the bridge from its own ACA app. Localhost won't work. You need one HTTPS URL (call it BRIDGE_PUBLIC_URL) that points at the bridge, and you set it in three places:

Where What
ServiceNow sys_property intranet_bridge.outbound_webhook_url <BRIDGE_PUBLIC_URL>/api/servicenow/webhook
teams_a2a env var BRIDGE_INTERNAL_URL <BRIDGE_PUBLIC_URL>
The browser (intranet page) Served from the same origin so relative paths work, or updated to use the absolute <BRIDGE_PUBLIC_URL>.

For local development a VS Code Dev Tunnel works fine. Helper scripts are checked in under scripts/:

# 1. Start the bridge container (publishes container :5000 on host :5001)
docker compose -f bridge/docker-compose.yml up -d --build

# 2. Create a persistent named tunnel (one-time)
.\scripts\devtunnel-create.ps1

# 3. Host it (Ctrl+C to stop; the tunnel itself persists)
.\scripts\devtunnel-host.ps1

Once the tunnel is up, set BRIDGE_PUBLIC_URL in bridge/.env to its HTTPS URL (e.g. https://<id>-5001.use.devtunnels.ms) and run:

# Pushes BRIDGE_PUBLIC_URL into ServiceNow's sys_property and into the
# Copilot Studio HTTP-tool botcomponents in one shot.
.\scripts\sync-bridge-url.ps1

That eliminates the manual UI edits whenever the tunnel URL changes (or whenever a different developer clones the repo and starts their own tunnel).

Or do it by hand:

devtunnel host --port-numbers 5001 --allow-anonymous

Copy the https://<id>-5001.use.devtunnels.ms URL it prints into the table above. Tunnel URLs change when you restart the tunnel unless you used a persistent named tunnel (the helper script does this for you) — when they do change, update the ServiceNow sys_property and the Copilot Studio HTTP action URL.

For anything beyond local dev, host the bridge on a real platform (Azure App Service, Azure Container Apps, Cloud Run, Fly.io, etc.) and use that platform's HTTPS URL.

Hosted reference deployment (Azure Container Apps)

The reference deployment runs the bridge as ca-cps-bridge in the cae-cpv Container Apps environment (rg-cpv-aca). One-shot deploy from a populated bridge/.env:

.\scripts\deploy-bridge-aca.ps1                  # full ACR build + ACA deploy
.\scripts\deploy-bridge-aca.ps1 -SkipBuild       # update existing image only

After the script reports Healthy, set BRIDGE_PUBLIC_URL=https://ca-cps-bridge.<env-suffix>.eastus2.azurecontainerapps.io in bridge/.env and re-run .\scripts\sync-bridge-url.ps1 to point ServiceNow + Copilot Studio at the new host.

The app is pinned to min=max=1 because the bridge holds session state in memory — see docs/03-bridge-backend.md and docs/09-production-hardening.md.

If anything goes sideways, jump to docs/07-troubleshooting.md — every entry in that table is a real failure mode that cost time to diagnose.

Why this repo exists

The official ServiceNow guidance for "external chat → AWA-routable interaction" is incomplete. Specifically:

  • AWA routing only fires when an interaction is linked to a sys_cs_conversation. Direct Table API inserts on interaction produce IMS records that never get routed.
  • sys_cs_conversation has zero out-of-the-box create ACLs, so external callers cannot insert into it directly.
  • The conversation must be created via sn_cs.VASystemObject.createConversation() (which populates a binary context blob); raw GlideRecord inserts produce conversations whose first agent reply throws a NullPointerException in ConversationContext.getBrandingKey().
  • Pushing a consumer message into a live chat via raw sys_cs_message insert persists the row but never publishes on AMB, so the agent's pane stays empty. The supported API is sn_cs.AgentChatScriptObject.send().

The Scripted REST scripts in servicenow/ encapsulate every one of those discoveries so you don't have to make them again.

Verified against

  • ServiceNow Yokohama (PDI, April 2026)
  • Microsoft Copilot Studio (April 2026)
  • Direct Line Channel (Bot Framework v3)
  • Python 3.12 / Flask 3.x / flask-sock
  • VS Code Dev Tunnels (for local development)

License

MIT — see LICENSE.