DirectLine JS Sample
A minimal, self-contained sample that connects to a Copilot Studio agent using Microsoft’s open-source botframework-directlinejs library — without WebChat. The UI is fully custom: plain HTML, CSS, and vanilla JavaScript.
What it demonstrates
- Token acquisition — fetch a DirectLine token from the Copilot Studio token endpoint (no authentication required for agents without enhanced security)
- Regional endpoint discovery — use the
regionalchannelsettingsAPI to get the correct DirectLine URL (agents are deployed regionally — never hardcodedirectline.botframework.com) - DirectLine connection — create a
DirectLineinstance with WebSocket transport - Connection status monitoring — react to
connectionStatus$(Online, ExpiredToken, FailedToConnect, etc.) - Greeting trigger — send a
startConversationevent to fire the agent’s Greeting topic - Receiving activities — subscribe to
activity$, filter byfrom.role === 'bot', and deduplicate - Markdown rendering — agent responses are markdown; rendered with the marked library
- Citation sources — extract citation metadata from
schema.org/Messageentities and display as a Sources footer - Suggested actions — render quick-reply buttons from
activity.suggestedActions - Typing indicators — animated dots while the agent is processing
Setup
-
Copy the config template and fill in your token endpoint:
cp config.sample.js config.jsTo find your token endpoint: Copilot Studio → Settings → Channels → Mobile app → copy the Token Endpoint URL.
-
Serve the files locally:
npx serve . -l 5510 -
Open http://localhost:5510
Code snippets
Connect to Copilot Studio
// Fetch token and regional endpoint (both derived from the token endpoint URL)
const tokenEndpoint = 'https://<env>.environment.api.powerplatform.com/.../directline/token?api-version=2022-03-01-preview';
const apiVersion = new URL(tokenEndpoint).searchParams.get('api-version');
const [{ channelUrlsById }, { token }] = await Promise.all([
fetch(new URL(`/powervirtualagents/regionalchannelsettings?api-version=${apiVersion}`, tokenEndpoint)).then(r => r.json()),
fetch(tokenEndpoint).then(r => r.json()),
]);
const directLine = new DirectLine.DirectLine({
token,
domain: new URL('v3/directline', channelUrlsById.directline).toString(),
webSocket: true,
});
Receive activities
directLine.activity$
.filter(activity => {
// Only agent activities — DirectLine echoes back your own messages
// with from.role undefined; the agent's always have from.role === 'bot'
return activity.from.role === 'bot';
})
.subscribe(activity => {
if (activity.type === 'message') console.log('Agent:', activity.text);
if (activity.type === 'typing') console.log('Agent is typing...');
});
Send a message
// postActivity returns an RxJS Observable — you MUST subscribe to trigger the send
directLine.postActivity({
from: { id: 'user1' },
type: 'message',
text: 'Hello!',
}).subscribe(
id => console.log('Sent, id:', id),
err => console.error('Error:', err),
);
Trigger the greeting topic
directLine.postActivity({
from: { id: 'user1' },
type: 'event',
name: 'startConversation',
}).subscribe();
Key gotchas
from.role, notfrom.id— DirectLine replaces yourfrom.idwith a server-assigned UUID. To distinguish agent activities from user echoes, filter onfrom.role === 'bot'(the protocol still uses “bot” as the role value).- Deduplication — DirectLine may deliver the same activity twice (WebSocket + polling fallback). Track seen activity IDs to prevent duplicate rendering.
- Lazy Observables —
postActivity()returns an RxJS Observable. You must call.subscribe()to actually send the request. - RxJS 5.x — the library bundles RxJS 5, which uses chainable
.filter().subscribe()(not the.pipe()syntax from RxJS 6+).
Files
| File | Description |
|---|---|
index.html |
Complete sample — HTML + CSS + JS in a single file |
config.sample.js |
Configuration template (token endpoint URL) |
config.js |
Your local config (gitignored) |