tejasadinen
ServiceNow Employee

🎯 The Adoption Opportunity

You've rolled out Now Assist, AI Agents, and Otto (Employee Slate). The platform team built it, leadership wants the ROI story, and the pieces are in place.

The teams that win this transformation all share one mindset:

"Let's make Otto the front door — and give people a clear path forward, no matter what they're trying to do."

That's the accelerator of AI ROI. Every great Otto rollout follows the same arc:

Approach What Happens
Keep classic ESC as default Safe, familiar — but the AI investment waits to shine
Switch to Otto with no safety net Bold and fast — works when every flow is covered
Switch to Otto + a graceful bridge Users explore with confidence → Every bridge click becomes a signal → Process team builds the missing use cases → Otto gets smarter every week

It's not "Otto or ESC" — it's "Otto, with a bridge back when needed." You only need one small widget to unlock it.

 

 


💡 The Widget

A Lit-based AIUX widget that:

  • ✨ Floats on every page in Employee Slate
  • 🎯 Drags anywhere — snaps to the nearest edge on release (iOS AssistiveTouch-style)
  • 💾 Remembers position during the session
  • ↪️ Navigates to /esc in the same tab
  • 📡 Fires a esc-footer-click event for analytics & surveys

Why this matters: Every click is a high-value signal — "This user was in Otto, and at this moment, they chose to go elsewhere." That's the most valuable input your AI roadmap can have.


⚙️ The Build

Step 1: Create the widget

Navigate to sys_aix_widget.list → New. Fill in:

Name ESC Footer Widget
ID esc-footer-widget
Category layout
Input Schema { "label": { "type": "String" }, "url": { "type": "String" } }

Step 2: Paste the Component code

import { html, css } from 'lit';
import { AIUXWidgetElement } from '@servicenow/aiux-components-core';

class EscFooterWidget extends AIUXWidgetElement {
    static properties = {
        label: { type: String },
        url:   { type: String },
    };

    constructor() {
        super();
        this.label = 'Switch to Employee Center';
        this.url   = '/esc';

        this._dragStartX = 0;
        this._dragStartY = 0;
        this._buttonStartX = 0;
        this._buttonStartY = 0;
        this._currentX = 0;
        this._currentY = 0;
        this._movedDistance = 0;
        this._isDragging = false;
        this._dragThreshold = 5;

        this._boundPointerMove = this._handlePointerMove.bind(this);
        this._boundPointerUp = this._handlePointerUp.bind(this);
    }

    firstUpdated() {
        this._btn = this.renderRoot.querySelector('.esc-fab');
        const labelEl = this.renderRoot.querySelector('.esc-fab__label');

        if (this._btn) {
            this._btn.setAttribute('aria-label', this.label);
            this._btn.addEventListener('pointerdown', this._handlePointerDown.bind(this));
            this._btn.addEventListener('click', this._handleClick.bind(this));
        }
        if (labelEl) labelEl.textContent = this.label;

        this._restorePosition();
    }

    _restorePosition() {
        try {
            const saved = sessionStorage.getItem('escFabPosition');
            if (saved) {
                const { x, y } = JSON.parse(saved);
                this._currentX = x;
                this._currentY = y;
                this._applyPosition(x, y);
            }
        } catch (e) {}
    }

    _savePosition(x, y) {
        try {
            sessionStorage.setItem('escFabPosition', JSON.stringify({ x, y }));
        } catch (e) {}
    }

    _applyPosition(x, y) {
        if (!this._btn) return;
        this._btn.style.left = x + 'px';
        this._btn.style.top = y + 'px';
        this._btn.style.right = 'auto';
        this._btn.style.bottom = 'auto';
    }

    _handlePointerDown(event) {
        if (event.button !== undefined && event.button !== 0) return;
        event.preventDefault();

        this._btn.setPointerCapture(event.pointerId);

        const rect = this._btn.getBoundingClientRect();
        this._dragStartX = event.clientX;
        this._dragStartY = event.clientY;
        this._buttonStartX = rect.left;
        this._buttonStartY = rect.top;
        this._movedDistance = 0;
        this._isDragging = false;

        this._btn.classList.remove('is-snapping');

        document.addEventListener('pointermove', this._boundPointerMove);
        document.addEventListener('pointerup', this._boundPointerUp);
    }

    _handlePointerMove(event) {
        const dx = event.clientX - this._dragStartX;
        const dy = event.clientY - this._dragStartY;
        this._movedDistance = Math.sqrt(dx * dx + dy * dy);

        if (!this._isDragging && this._movedDistance > this._dragThreshold) {
            this._isDragging = true;
            this._btn.classList.add('is-dragging');
        }

        if (this._isDragging) {
            const padding = 8;
            const btnWidth = this._btn.offsetWidth;
            const btnHeight = this._btn.offsetHeight;
            const maxX = window.innerWidth - btnWidth - padding;
            const maxY = window.innerHeight - btnHeight - padding;

            let newX = this._buttonStartX + dx;
            let newY = this._buttonStartY + dy;

            newX = Math.max(padding, Math.min(newX, maxX));
            newY = Math.max(padding, Math.min(newY, maxY));

            this._currentX = newX;
            this._currentY = newY;
            this._applyPosition(newX, newY);
        }
    }

    _handlePointerUp(event) {
        document.removeEventListener('pointermove', this._boundPointerMove);
        document.removeEventListener('pointerup', this._boundPointerUp);
        if (this._isDragging) this._snapToEdge();
    }

    _snapToEdge() {
        const padding = 16;
        const btnWidth = this._btn.offsetWidth;
        const viewportCenter = window.innerWidth / 2;
        const btnCenter = this._currentX + (btnWidth / 2);

        const snapX = btnCenter < viewportCenter
            ? padding
            : window.innerWidth - btnWidth - padding;

        this._btn.classList.remove('is-dragging');
        this._btn.classList.add('is-snapping');

        this._currentX = snapX;
        this._applyPosition(snapX, this._currentY);
        this._savePosition(snapX, this._currentY);

        setTimeout(() => {
            this._btn.classList.remove('is-snapping');
            this._isDragging = false;
        }, 350);
    }

    _handleClick(event) {
        event.preventDefault();
        event.stopPropagation();

        if (this._movedDistance > this._dragThreshold) return;

        this.dispatchEvent(new CustomEvent('esc-footer-click', {
            bubbles: true,
            composed: true,
            detail: { url: this.url, timestamp: new Date().toISOString() },
        }));

        window.location.assign(this.url);
    }

    render() {
        return html`
            <div class="esc-fab" role="button" tabindex="0">
                <span class="esc-fab__label"></span>
                <svg class="esc-fab__icon" xmlns="http://www.w3.org/2000/svg"
                     width="12" height="12" viewBox="0 0 20 20" aria-hidden="true">
                    <path fill="currentColor"
                          d="M16.5 3a.5.5 0 0 1 .5.5v10a.5.5 0 0 1-1 0V4.707L3.854 16.853a.5.5 0 0 1-.707-.707L15.293 4H6.5a.5.5 0 0 1 0-1h10Z"/>
                </svg>
            </div>
        `;
    }

    static styles = css`
        :host {
            display: contents;
            font-family: var(--font-sans, Inter, "Servicenow Sans", sans-serif);
        }
        .esc-fab {
            position: fixed; bottom: 24px; right: 24px; z-index: 9999;
            display: inline-flex; align-items: center;
            gap: calc(var(--spacing, 0.25rem) * 1);
            padding: calc(var(--spacing, 0.25rem) * 1.5) calc(var(--spacing, 0.25rem) * 3.5);
            background: var(--color-base-100, #ffffff);
            color: var(--color-base-content, #1e293b);
            border: 1px solid var(--color-base-300, #e5e5e5);
            border-radius: var(--radius-full, 9999px);
            font-size: var(--text-xs, 0.75rem);
            font-weight: var(--font-weight-medium, 500);
            cursor: grab; white-space: nowrap;
            user-select: none; touch-action: none; -webkit-user-select: none;
            box-shadow: 0 3px 8px rgba(0,0,0,0.07), 0 1px 3px rgba(0,0,0,0.04);
            transition: box-shadow 0.2s ease, background 0.2s ease;
        }
        .esc-fab:hover:not(.is-dragging):not(.is-snapping) {
            box-shadow: 0 8px 20px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.06);
        }
        .esc-fab.is-dragging {
            cursor: grabbing; transition: none;
            transform: scale(1.05);
            box-shadow: 0 16px 32px rgba(0,0,0,0.20), 0 8px 12px rgba(0,0,0,0.12);
            opacity: 0.95;
            border-color: var(--color-primary, #003B54);
        }
        .esc-fab.is-snapping {
            transition: left 0.35s cubic-bezier(0.22, 1, 0.36, 1),
                        top 0.35s cubic-bezier(0.22, 1, 0.36, 1),
                        transform 0.35s ease, box-shadow 0.35s ease;
            transform: scale(1);
        }
        .esc-fab:focus-visible {
            outline: 2px solid var(--color-primary, #003B54);
            outline-offset: 3px;
        }
        .esc-fab__icon {
            display: inline-block; flex-shrink: 0;
            opacity: 0.7; transition: opacity 0.2s ease; pointer-events: none;
        }
        .esc-fab__label { pointer-events: none; }
        .esc-fab:hover:not(.is-dragging) .esc-fab__icon { opacity: 1; }
        @media (max-width: 480px) {
            .esc-fab {
                bottom: 16px; right: 16px;
                padding: calc(var(--spacing, 0.25rem) * 1) calc(var(--spacing, 0.25rem) * 2.5);
            }
        }
        @media (prefers-reduced-motion: reduce) {
            .esc-fab, .esc-fab__icon, .esc-fab.is-snapping { transition: none; }
            .esc-fab.is-dragging { transform: none; }
        }
    `;
}

Step 3: Bind it to Employee Slate

  1. Navigate to your default experience in sys_aix_experience
  2. Open the App Shell record mapped to that experience in sys_aix_app_shell
  3. Add esc-footer-widget to the Footer field

That's it — refresh Employee Slate and the floating bridge is live on every page.


📡 Capturing the Signal

Listen for the bridge event anywhere on the page:

document.addEventListener('esc-footer-click', (e) => {
    GlideAjax.send('fallback_capture_api', {
        timestamp: e.detail.timestamp,
        page: window.location.pathname,
        user: window.NOW.user.userID
    });
});

From there, route to your survey, your analytics dashboard, or both.


📊 Closing the Loop

  1. Build a "Bridge Insights" dashboard — group bridge clicks by page, role, and time
  2. Auto-trigger a 2-question survey: "What were you trying to do?"
  3. Feed responses into your AI use-case backlog — pre-validated by real users
  4. Track the bridge rate over time — watch it decline as you ship the missing flows

💎 Three months in, you'll be able to show leadership exactly which AI investments delivered — by pointing to the use cases that moved from "bridge hot spots" to "fully Otto-handled." That's the ROI story that funds the next investment.


💬 Let's Discuss

  • 🤝 AEs/CSEs: What's working for your customers in their Otto rollouts?
  • 🛠️ Developers: Built something similar? Share your pattern.
  • 📈 AI/Process leads: How are you identifying new AI use cases today?

If this gave you an idea you can use, hit 👍 and share it with your customer success team. Every team that ships this pattern shortens the road to Otto adoption.


About the Author: Senior Technical Support Engineer at ServiceNow on the ArcX team. CSA, CAD, CIS-HRSD, and ServiceNow Architecture Excellence certified. Two-time LLAMA Award recipient.

#NowAssist #EmployeeSlate #Otto #AIAdoption #LitWebComponents #ServiceNowDeveloper #AIUX