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)
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.
| 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). |
- Read
docs/01-architecture.md(5 min). - Work through
docs/02-servicenow-setup.mdin your dev PDI. End state: aPOSTto the scripted REST/open_chatendpoint returns a routedIMS#######and your test agent gets a chat invite in their Inbox. - Run the bridge (
docs/03-bridge-backend.md) locally, exposed via a tunnel. - Configure the two Copilot Studio agents and register
teams_a2aas a Connected Agent on each (docs/04-copilot-studio.md). - Run the end-to-end probe in
docs/06-end-to-end-test.md.
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.ps1Once 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.ps1That 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-anonymousCopy 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.
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 onlyAfter 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.
The official ServiceNow guidance for "external chat → AWA-routable interaction" is incomplete. Specifically:
- AWA routing only fires when an
interactionis linked to asys_cs_conversation. Direct Table API inserts oninteractionproduce IMS records that never get routed. sys_cs_conversationhas 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 binarycontextblob); raw GlideRecord inserts produce conversations whose first agent reply throws aNullPointerExceptioninConversationContext.getBrandingKey(). - Pushing a consumer message into a live chat via raw
sys_cs_messageinsert persists the row but never publishes on AMB, so the agent's pane stays empty. The supported API issn_cs.AgentChatScriptObject.send().
The Scripted REST scripts in servicenow/ encapsulate every
one of those discoveries so you don't have to make them again.
- 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)
MIT — see LICENSE.
