Achievement
A rule-driven achievement engine: admins define achievements with typed rules (counters, thresholds, streaks, accumulators, collections, composites, time-limited), clients post events, and the engine evaluates progress, awards points, cascades prerequisite unlocks, and streams live progress over WebSocket.
When to reach for it: badge walls, fitness and learning milestones, seasonal medals, engagement awards ("first post"), SaaS adoption gamification ("first 1,000 API requests") - recognition for what already happened.
When not to: assigned multi-step arcs with acceptance and turn-in belong to Quest; raw score rankings belong to Leaderboard - the built-in achievement board only ranks achievement points.
Concepts
- Definitions are admin-only and write-once - define, disable, grant, and revoke require a platform admin token. There is no update, delete, or re-enable: retiring an achievement means disabling it.
- Rules decide progress - each definition carries one rule (possibly a composite of nested rules). The seven rule types have their own page: Rule types.
- Events drive everything -
POST /eventsmatches an event against every active achievement of the acting user, which defaults to the caller; only admins may submit for someone else. - States -
locked -> available -> in_progress -> completed(orexpiredfor overdue time-limited rules). An achievement withprerequisitesstartslockedand becomesavailableautomatically when the prerequisites complete. - Points feed a leaderboard - completion awards the definition's
pointsto a global achievement-points board. Therewardspayload (items, currency, titles) is stored on the definition but not applied; points are the only reward the engine acts on today. - Hidden achievements are excluded from discovery listings by default,
but this is UI-level concealment, not authorization: any caller may pass
include_hidden, and a user's own profile lists them before unlock. - Dedupe keys make event submission idempotent per user for the dedupe TTL; a replay succeeds but processes nothing.
Define achievements (requires a platform admin token)
snug achievement define --id first-strike --name "First Strike" \
--description "Defeat three enemies" --points 100 \
--category combat --tier bronze \
--rules '{"type":"counter","event":"enemy.defeated","target":3}'
snug achievement define --id high-score --name "High Score" \
--description "Reach a score of 1000" --points 150 \
--category combat --tier silver \
--rules '{"type":"threshold","metric":"score","value":1000,"comparison":"gte"}'The response reports the initial_state every user will start in -
available, or locked when --prerequisite is set. Reusing an id fails
with 409 achievement_exists; a non-admin caller gets 403 FORBIDDEN.
Reading a definition back (snug achievement get --id first-strike)
returns only the public card - id, name, description, points, xp, category,
tier, hidden. Rules, prerequisites, rewards, and metadata are write-only
through the API, so keep your definitions in version control.
Drive events
snug achievement event --event-type enemy.defeated --event-data '{"enemy":"goblin"}'
snug achievement event --event-type enemy.defeated --user-id player123 # admin only
snug achievement batch --events '[{"event_type":"enemy.defeated","event_data":{}}]'Over HTTP the same submission is POST /api/v1/achievements/events;
the enveloped response lists every achievement the event advanced
(captured live, trimmed):
{
"status": 200,
"msg": "OK",
"data": {
"event_id": "KvqFNEysERvC",
"processed_achievements": [
{
"achievement_id": "docs4-marathon",
"previous_state": "available",
"new_state": "in_progress",
"progress": { "type": "accumulator", "current": 5.0, "target": 42.0, "percentage": 11.904762 },
"completed": false
}, ...
],
"unlocked_achievements": [],
"webhooks_triggered": 0,
"leaderboard_updated": false
}
}Two fields are easy to misread, both verified live:
- A completion appears in
processed_achievementswithcompleted: truepluspoints_awardedandxp_awarded.unlocked_achievementsis something else: achievements whose prerequisites this completion just satisfied (lockedbecameavailable). - Replaying a
--dedupe-keythe user already submitted returns200with empty arrays - a silent no-op, not an error. Failed processing releases the key so a retry works.
Batches run per event in order and are capped (100 events by default);
an oversized batch fails with 400 events: length is greater than 100.
A non-admin submitting for another user gets 403 FORBIDDEN.
Track progress
snug achievement in-progress --user-id player123 # progress + next_milestone
snug achievement user --user-id player123 # full profile with stats
snug achievement leaderboard --limit 10
snug achievement stats --id first-strikeProfile and in-progress queries are self-or-admin; another user's profile
is 403 FORBIDDEN. The profile aggregates totals and per-category stats:
{ "total_points": 250, "total_xp": 0, "completed_count": 2,
"in_progress_count": 1, "completion_percentage": 50.0 }Per-achievement stats reports attempted/completed counts, completion
rate, and a state distribution. The leaderboard ranks total achievement
points and includes the caller's own user_rank (tier_breakdown in its
entries is currently always empty).
Listing and discovery (snug achievement list --category combat --tier bronze) supports q, filter, and sort_by from the
shared search grammar, plus category,
tier, and include_hidden shortcuts.
Grant, revoke, disable (requires a platform admin token)
snug achievement grant --id first-strike --user-id player123 --reason "Compensation"
snug achievement revoke --id first-strike --user-id player123 --reason "Cheating" --remove-points
snug achievement disable --id first-strike --reason "Season ended" --hide-from-uiGrant forces completion and awards points. Revoke resets the user's state
and progress; --remove-points also takes the points back and resyncs the
leaderboard. Disable stops all further progress (verified: events that
previously advanced the achievement process nothing afterwards), and
--hide-from-ui additionally marks it hidden; disabled definitions stay
readable and stay in users' profiles.
Live progress over WebSocket
GET /api/v1/achievements/users/{user_id}/live upgrades to a WebSocket
that streams progress frames as events process (self-or-admin; a
cross-user handshake is rejected with 403). The CLI wraps it as
snug achievement watch --user-id docs-wave; captured live:
{"type":"pubsub_message","channel":"achievements.docs-wave.live",
"payload":{"type":"achievement_progress","achievement_id":"docs4-secret-room",
"previous_state":"available","new_state":"completed","completed":true,
"points_awarded":50, "progress":{"type":"counter","current":1,"target":1,
"percentage":100.0}, ...}}A second frame announces the completion event itself; the full message protocol is in the API reference.
Limits and configuration
Batch size (100 events), dedupe TTL (1 hour), rules per achievement (10),
and the expiry-sweep interval for time-limited rules (60 s) are tunable via
the ACHIEVEMENT_* variables in the
Achievement CONFIG reference.
Reference
- Achievements API - every endpoint, callable
- Rule types - all seven rule types with live-verified progress payloads
- Related: Quest for assigned objectives with turn-in, Leaderboard for general-purpose rankings