Most ServiceNow hackathon demos look the same: a catalog item with an approval flow, a Flow Designer automation that closes tickets a little faster, a dashboard nobody asked for. A few of us wanted to try something different for our next internal hackathon — could we build something that's actually fun to demo, while still exercising real Service Portal development skills? That question turned into a small browser game called Memory Match, built entirely as a native ServiceNow widget.
The idea itself is simple: a flip-card memory game with a live moves/time tracker and a saved leaderboard. What makes it a genuinely useful hackathon project isn't the game concept — it's that building one small, self-contained feature forces you through nearly every layer of Now Platform widget development: client-side state management, GlideRecord operations, ACL decisions, and the HTML/CSS/client/server split that every production widget shares.
From there, we sketched out a project plan the same way we would for any other hackathon build.
Project Plan for the Memory Match Widget
1. Requirements Gathering
- Understand what a Service Portal widget can and can't do out of the box.
- Define what the game needs to actually work:
- A card grid that flips and checks for matches, entirely client-side for responsiveness.
- A moves counter and a running timer.
- A win state that lets the player save their score.
- A persisted leaderboard, stored server-side so it survives page reloads and is visible to everyone.
- A custom table to hold scores, since nothing in the base schema fits.
- Sensible ACLs — this is worth flagging early: the moment you add write access for anonymous or lightly-authenticated users, you need to think through who can insert records and whether the client can be trusted to self-report a score without any server-side sanity checking.
2. Technology Stack
- Frontend: Service Portal's built-in AngularJS 1.x widget framework — HTML template plus a client controller, no external UI library needed.
- Backend: the widget's native Server Script, running standard server-side JavaScript.
- Data: a custom table (
u_memory_game_score) accessed through GlideRecord. - Hosting: none needed — it runs entirely inside the existing ServiceNow instance, on Service Portal.
3. Design and Architecture
- HTML Template: the card grid, stats bar, win screen, and leaderboard table — all driven by Angular bindings against the client controller's scope.
- Client Controller: owns all real-time gameplay — card flips, match checking, the timer — so nothing needs a server round-trip while the player is actually playing.
- Server Script: only gets involved twice — once to save a finished score, once to fetch the current leaderboard.
- CSS: a simple 3D flip transform for the cards, kept in the widget's own CSS field so it doesn't leak into the rest of the portal theme.
4. Implementation Steps
- Create the table:
u_memory_game_scorewith player name, moves, and seconds fields. - Build the widget shell: new Service Portal widget, wire up the four fields — HTML Template, CSS, Client Controller, Server Script.
- Implement gameplay: shuffle a deck, handle flip/match logic, run the timer with
$interval. - Wire up persistence: on win, call
c.server.get()to save the score and pull back a fresh leaderboard. - Add the page: drop the widget onto a portal page through Service Portal Designer.
- Test: confirm the full loop — play, win, save name, see the leaderboard update — works end to end, on both desktop and mobile.
5. Deployment and Maintenance
- Build and test it in a personal developer instance first (free at developer.servicenow.com) before touching anything production-facing.
- Move it between instances with a standard Update Set, like any other customization.
- Review the table's ACLs periodically — especially if it ever moves from an internal demo to something with wider portal exposure.
With the plan settled, here's what the actual widget code looks like — the same pattern you'd use for any Service Portal widget, just applied to a game instead of a form.
The Widget, Piece by Piece
The HTML Template lays out the card grid, stats, and leaderboard using standard Angular directives:
<div class="mg-board" ng-if="!c.won">
<div class="mg-card"
ng-repeat="card in c.cards track by $index"
ng-class="{flipped: card.flipped || card.matched}"
ng-click="c.flipCard($index)">
<div class="mg-card-inner">
<div class="mg-card-front">?</div>
<div class="mg-card-back">{{card.icon}}</div>
</div>
</div>
</div>
The Client Controller handles every flip, entirely in the browser — no server call while the player is mid-game:
c.flipCard = function(index) {
if (lockBoard) return;
var card = c.cards[index];
if (card.flipped || card.matched) return;
card.flipped = true;
flippedIndexes.push(index);
if (flippedIndexes.length === 2) {
c.moves++;
lockBoard = true;
var first = c.cards[flippedIndexes[0]];
var second = c.cards[flippedIndexes[1]];
if (first.icon === second.icon) {
first.matched = true;
second.matched = true;
flippedIndexes = [];
lockBoard = false;
checkWin();
} else {
setTimeout(function() {
$scope.$apply(function() {
first.flipped = false;
second.flipped = false;
flippedIndexes = [];
lockBoard = false;
});
}, 800);
}
}
};
And the Server Script is where GlideRecord does the only two things it needs to: save a score, and return the current top ten.
function getLeaderboard() {
var results = [];
var gr = new GlideRecord('u_memory_game_score');
gr.orderBy('u_moves');
gr.orderBy('u_seconds');
gr.setLimit(10);
gr.query();
while (gr.next()) {
results.push({
name: gr.getValue('u_player_name'),
moves: gr.getValue('u_moves'),
seconds: gr.getValue('u_seconds')
});
}
return results;
}
if (input.action === 'save_score') {
var gr = new GlideRecord('u_memory_game_score');
gr.initialize();
gr.setValue('u_player_name', input.name);
gr.setValue('u_moves', input.moves);
gr.setValue('u_seconds', input.seconds);
gr.insert();
data.leaderboard = getLeaderboard();
} else if (input.action === 'get_leaderboard') {
data.leaderboard = getLeaderboard();
}
That's the whole architecture: client owns the interaction, server owns the record of truth. It's a pattern you'll reuse constantly in real widget development — usually dressed up as "request status" or "approval state" instead of a game score.
Why This Works Well as a Hackathon Project
- It demos itself. Judges don't need a walkthrough — they can just play it.
- A working v1 is fast. A minimal playable version, no leaderboard, can be done in an hour or two.
- It's memorable. Weeks later, "the team that built a game" outlasts "the team that streamlined another form" in people's memory.
- It scales with skill level. Beginners ship a working game. More experienced developers can add difficulty scaling, real-time multiplayer, or tie the leaderboard into actual Performance Analytics data.
Additional Tips If You Try This
- Lock down the leaderboard table: restrict write access appropriately once this leaves a sandbox — don't leave it open to any authenticated user by default.
- Reuse the pattern for real training tools: the same widget skeleton works for an onboarding quiz, an incident-response speed drill, or a leaderboard tied to real ticket-resolution metrics.
- Keep gameplay client-side: anything that needs to feel instant — flips, timers, animations — should never wait on a server round-trip.
If your next hackathon team is stuck between "another approval flow" and "something people will actually remember," build the game. It touches the same core skills as any production widget — just with a much better demo.
