Building a CRM Productivity Game with p5.js
Build a gamified CRM productivity game in DenchClaw using p5.js — earn points for completing follow-ups, advancing deals, and hitting daily goals.
Building a CRM Productivity Game with p5.js
Sales motivation is a real problem. Staring at a CRM table and grinding through follow-ups isn't inherently fun. But what if completing a follow-up spawned a particle burst? What if advancing a deal to "Closed Won" triggered an animation? What if your daily call goal was a health bar depleting in real time?
DenchClaw's App Builder lets you put p5.js inside a .dench.app folder with direct access to your CRM data. This guide builds a lightweight productivity game where your CRM actions earn you in-game points and visual rewards.
What You're Building#
- A p5.js canvas game that reads your CRM activity
- Points system: +5 for logging a follow-up, +20 for advancing a deal, +50 for closing a deal
- Daily progress bar toward a configurable goal
- Particle effects triggered by CRM events
- Leaderboard if multiple reps use the same DenchClaw instance
Step 1: App Setup#
mkdir -p ~/.openclaw-dench/workspace/apps/crm-game.dench.app.dench.yaml:
name: CRM Game
description: Gamified CRM activity tracker with p5.js rewards
icon: zap
version: 1.0.0
permissions:
- read:crm
display: tabStep 2: HTML with p5.js#
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CRM Game</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/p5.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0f172a; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; min-height: 100vh; padding: 20px; font-family: system-ui, sans-serif; }
#stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; width: 100%; max-width: 800px; margin-bottom: 20px; }
.stat { background: #1e293b; border-radius: 12px; padding: 16px; text-align: center; }
.stat-value { font-size: 32px; font-weight: 800; color: #e2e8f0; }
.stat-label { font-size: 11px; color: #64748b; text-transform: uppercase; margin-top: 4px; }
#gameCanvas { border-radius: 12px; overflow: hidden; }
#activityLog { width: 100%; max-width: 800px; margin-top: 16px; background: #1e293b; border-radius: 12px; padding: 16px; max-height: 150px; overflow-y: auto; }
.log-item { font-size: 12px; color: #94a3b8; padding: 3px 0; border-bottom: 1px solid #334155; }
.log-item.big-win { color: #10b981; font-weight: 700; }
</style>
</head>
<body>
<div id="stats">
<div class="stat"><div class="stat-value" id="todayScore">0</div><div class="stat-label">Today's Points</div></div>
<div class="stat"><div class="stat-value" id="streak">0</div><div class="stat-label">Day Streak</div></div>
<div class="stat"><div class="stat-value" id="followups">0</div><div class="stat-label">Follow-ups Today</div></div>
<div class="stat"><div class="stat-value" id="dealsWon">0</div><div class="stat-label">Deals Won</div></div>
</div>
<canvas id="gameCanvas"></canvas>
<div id="activityLog"></div>
<script src="game.js"></script>
</body>
</html>Step 3: p5.js Game Logic#
game.js:
const DAILY_GOAL = 200; // points
const CANVAS_W = 800;
const CANVAS_H = 300;
let todayScore = 0;
let particles = [];
let goalReached = false;
let stars = [];
let activityData = {};
// p5.js sketch
const sketch = (p) => {
p.setup = function() {
const canvas = p.createCanvas(CANVAS_W, CANVAS_H);
canvas.parent('gameCanvas');
// Create background stars
for (let i = 0; i < 80; i++) {
stars.push({ x: p.random(CANVAS_W), y: p.random(CANVAS_H), size: p.random(1, 3), twinkle: p.random(1000) });
}
};
p.draw = function() {
p.background(15, 23, 42);
// Draw stars
stars.forEach(star => {
const brightness = 128 + 64 * Math.sin((p.millis() + star.twinkle) / 800);
p.fill(brightness, brightness, brightness + 40, brightness);
p.noStroke();
p.ellipse(star.x, star.y, star.size);
});
// Progress bar background
p.fill(30, 41, 59);
p.noStroke();
p.rect(40, CANVAS_H - 60, CANVAS_W - 80, 24, 12);
// Progress fill
const progress = Math.min(todayScore / DAILY_GOAL, 1);
const fillWidth = (CANVAS_W - 80) * progress;
if (progress >= 1) {
// Rainbow gradient when goal reached
for (let i = 0; i < fillWidth; i++) {
const hue = (i / fillWidth * 360 + p.millis() / 10) % 360;
p.colorMode(p.HSB);
p.fill(hue, 80, 90);
p.rect(40 + i, CANVAS_H - 60, 1, 24, 0);
}
p.colorMode(p.RGB);
} else {
p.fill(99, 102, 241);
p.rect(40, CANVAS_H - 60, fillWidth, 24, 12, 0, 0, fillWidth < 24 ? 12 : 0);
}
// Progress label
p.fill(148, 163, 184);
p.noStroke();
p.textSize(12);
p.textAlign(p.LEFT);
p.text(`Daily Goal: ${todayScore} / ${DAILY_GOAL} pts`, 40, CANVAS_H - 70);
// Score in center of bar
p.fill(255);
p.textAlign(p.CENTER);
p.textSize(12);
p.text(`${Math.round(progress * 100)}%`, CANVAS_W / 2, CANVAS_H - 43);
// Draw and update particles
for (let i = particles.length - 1; i >= 0; i--) {
const pt = particles[i];
pt.y -= pt.vy;
pt.x += pt.vx;
pt.life -= 2;
pt.vy -= 0.05; // gravity
p.fill(pt.r, pt.g, pt.b, pt.life);
p.noStroke();
p.ellipse(pt.x, pt.y, pt.size * (pt.life / 255));
if (pt.life <= 0) particles.splice(i, 1);
}
// Achievement text
if (goalReached) {
p.fill(16, 185, 129);
p.textAlign(p.CENTER);
p.textSize(28);
p.text('🎯 DAILY GOAL REACHED!', CANVAS_W / 2, 80);
}
};
};
function spawnParticles(x, y, color, count = 20) {
const [r, g, b] = color;
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 4,
vy: Math.random() * 3 + 2,
size: Math.random() * 8 + 4,
r, g, b,
life: 255
});
}
}
function addLog(message, isBigWin = false) {
const log = document.getElementById('activityLog');
const item = document.createElement('div');
item.className = `log-item${isBigWin ? ' big-win' : ''}`;
item.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
log.insertBefore(item, log.firstChild);
if (log.children.length > 50) log.removeChild(log.lastChild);
}
function awardPoints(points, reason, x = CANVAS_W / 2, y = CANVAS_H / 2, isBigWin = false) {
todayScore += points;
document.getElementById('todayScore').textContent = todayScore;
addLog(`+${points} pts — ${reason}`, isBigWin);
const color = isBigWin ? [16, 185, 129] : [99, 102, 241];
spawnParticles(x, y, color, isBigWin ? 50 : 20);
if (todayScore >= DAILY_GOAL && !goalReached) {
goalReached = true;
spawnParticles(CANVAS_W / 2, CANVAS_H / 2, [251, 191, 36], 100);
addLog('🎯 DAILY GOAL REACHED! Amazing work!', true);
}
}
async function loadTodayActivity() {
const today = new Date().toISOString().split('T')[0];
// Count today's follow-ups (entries updated today)
const followups = await dench.db.query(`
SELECT COUNT(*) as count FROM entries
WHERE DATE(updated_at) = '${today}'
AND object_id IN (SELECT id FROM objects WHERE name = 'people')
`);
const followupCount = followups[0]?.count || 0;
document.getElementById('followups').textContent = followupCount;
// Count deals closed this month
const wonDeals = await dench.db.query(`
SELECT COUNT(*) as count FROM v_deals WHERE "Stage" = 'Closed Won'
AND DATE("Close Date") >= DATE_TRUNC('month', CURRENT_DATE)
`);
document.getElementById('dealsWon').textContent = wonDeals[0]?.count || 0;
// Award initial points for today's work
const baseScore = followupCount * 5;
if (baseScore > 0) {
todayScore = baseScore;
document.getElementById('todayScore').textContent = todayScore;
addLog(`Loaded: ${followupCount} follow-ups completed today (+${baseScore} pts)`);
}
// Load streak from store
const streak = await dench.store.get('activity_streak') || 0;
document.getElementById('streak').textContent = streak;
}
// Listen for real-time CRM events to award points
dench.events.on('entry:updated', async (event) => {
awardPoints(5, 'Contact updated');
});
dench.events.on('entry:created', async (event) => {
awardPoints(10, 'New contact/deal added');
});
// Initialize p5 and load data
new p5(sketch);
loadTodayActivity();Extending the Game#
Achievements: Add milestone achievements like "First deal this week" or "5 follow-ups in one day" that trigger special animations.
Team leaderboard: If multiple reps use DenchClaw, query each rep's activity and display a ranked leaderboard.
Weekly challenges: Use dench.cron to reset weekly goals and surface new challenges each Monday.
Frequently Asked Questions#
How do I customize the daily goal?#
Change the DAILY_GOAL constant at the top of game.js. You can also make it configurable via dench.store: const DAILY_GOAL = await dench.store.get('daily_goal') || 200.
Can I track specific actions like calls or emails?#
Yes. If you have a custom "Activity" object in your CRM that logs calls and emails, query it and award points per activity type with different point values.
Does this work on mobile?#
p5.js canvas works on mobile. The main limitation is touch input — if you add interactive elements, test them on mobile. For a widget version, show just the progress bar without the full p5 canvas.
Can multiple team members use this simultaneously?#
Yes. Each user's DenchClaw instance has its own score state. For shared team scores, store totals in a shared DuckDB table and aggregate them in the leaderboard query.
Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →
