ServicesAchievement

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 /events matches 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 (or expired for overdue time-limited rules). An achievement with prerequisites starts locked and becomes available automatically when the prerequisites complete.
  • Points feed a leaderboard - completion awards the definition's points to a global achievement-points board. The rewards payload (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_achievements with completed: true plus points_awarded and xp_awarded. unlocked_achievements is something else: achievements whose prerequisites this completion just satisfied (locked became available).
  • Replaying a --dedupe-key the user already submitted returns 200 with 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-strike

Profile 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-ui

Grant 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

On this page