Quest
An engine for assigned, tracked, turn-in goals: definitions with ordered or parallel steps, progress driven by posted events, prerequisites and branching chains, daily/weekly repeats, expiry windows, and rewards paid out on turn-in through Treasury, Item Engine, and Achievements. Three genres share one model: objective quests, timed expeditions, and puzzle chains.
When to reach for it: MMO-style quest lines and dailies, SaaS onboarding checklists, idle-game expeditions, compliance training with deadlines, escape rooms and ARGs with hint economies.
When not to: passive recognition of what already happened - a badge with no accept/turn-in loop - belongs to Achievements; background work queues with retries belong to Job Queue.
Concepts
- Definitions vs states - an admin authors a
QuestDefinition; each user gets their ownQuestStateper quest per cycle. Updates bump the definitionversion; in-flight states pin the version they accepted. - Three kinds -
objective(steps advanced by events),expedition(dispatch a party, resolve on return),puzzle(steps completed by validated answers) - the latter two have their own page. - Lifecycle - accepting an offered quest moves it straight to
in_progress; it ends asturned_in,expired, orabandoned, and rewards only ever pay out on an explicit turn-in. - Events drive progress - one ingestion endpoint matches posted events
against all of the caller's active quests by
event_typeplus exact-equality payload predicates; idempotency keys make replays no-ops. - Cycle keys make repeats work - a non-repeatable quest lives in cycle
static; adaily/weeklyschedule keys cycles by UTC date or ISO week. Each quest is acceptable once per cycle. - Prerequisites and branching -
prerequisites.all_ofhides a quest until the listed quests are turned in;branchingentries withon: "turned_in"report follow-up unlocks in the turn-in response.
Authoring definitions
Creating, updating, and deleting definitions requires a platform admin token; listing and getting are open to any authenticated user.
snug quest definitions create --file ./forge.json
snug quest definitions list --kind objective --query forge
snug quest definitions get --quest-id forge
snug quest definitions update --quest-id forge --file ./patch.json # partial; bumps version
snug quest definitions delete --quest-id forgeA minimal objective definition - two ordered steps, a reward, a deadline:
{
"quest_id": "forge",
"name": "Forge Apprentice",
"kind": "objective",
"step_mode": "ordered",
"steps": [
{ "id": "gather", "name": "Gather iron ore", "objective": { "event_type": "item.acquired", "match": { "item_id": "iron_ore" }, "target_count": 3 } },
{ "id": "smelt", "name": "Smelt an iron bar", "objective": { "event_type": "item.crafted", "match": { "item_id": "iron_bar" }, "target_count": 1 } }
],
"rewards": [ { "type": "treasury", "currency": "gold", "amount": 50 } ],
"expiry": { "complete_window_seconds": 3600 }
}Offered, accept, log
offered returns every quest whose prerequisites you satisfy and which
you have not yet accepted this cycle. Accepting seeds per-step progress
and stamps expires_at; the raw POST returns the enveloped state
(captured live, trimmed):
curl -s -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
http://localhost:4000/api/v1/quests/forge/accept{
"status": 200,
"msg": "OK",
"data": {
"quest_id": "forge",
"status": "in_progress",
"expires_at": 1787927328,
"step_progress": {
"gather": { "count": 0, "completed": false },
"smelt": { "count": 0, "completed": false }
},
"current_step": "gather",
"cycle_key": "static"
}
}Re-accepting in the same cycle is 409 already_accepted - and so is
accepting again after abandoning: the cycle's state record remains, so
abandonment is final for the cycle (a daily comes back tomorrow).
Unmet prerequisites are 403 prerequisites_not_met.
snug quest log --status in_progress shows your states with per-step
counts; statuses are in_progress, turned_in, expired, abandoned.
Progress events
snug quest events emit --type item.acquired \
--payload '{"item_id":"iron_ore"}' --idempotency-key evt-1{ "deltas": [ { "quest_id": "forge", "step_id": "gather", "count": 1,
"step_completed": false, "ready_for_turn_in": false } ] }The response lists only the steps the event actually advanced. Verified:
- Ordered gating - in
step_mode: orderedonly the current step matches; anitem.craftedposted while still ongatherreturns empty deltas.parallelsteps all match at once. - Match predicates are exact equality -
{"item_id": "copper_ore"}does not advance a step matching oniron_ore. - Idempotency - re-emitting a seen
idempotency_keyreturns empty deltas and changes nothing. - Lazy expiry - no scheduler; a quest past
expires_atflips toexpiredwhen an event or turn-in touches it, and stops matching.
The HTTP endpoint (POST /api/v1/quests/events) takes an array of up to
50 events. With a platform admin token, subject_user_id applies the
events to another user - the service-to-service path; non-admins get 403.
Turn-in and rewards
snug quest turn-in --quest-id forge{
"quest_id": "forge",
"status": "turned_in",
"rewards": [
{ "reward": { "type": "treasury", "currency": "gold", "amount": 50 }, "status": "granted" }
],
"unlocked": []
}Turn-in is atomic and exactly-once: incomplete steps are
409 steps_incomplete, a second turn-in is 409 already_turned_in, and
past the deadline it is 409 quest_expired. Rewards fan out to
Treasury (type: treasury),
Item Engine (type: item), and
Achievements
(type: achievement) through a durable per-line intent - each line comes
back granted, failed, or pending; if the fanout is interrupted,
snug quest resume-rewards --quest-id forge re-drives the pending lines.
unlocked lists quest ids from branching rules with on: "turned_in" -
pair them with prerequisites.all_of on the next act to build chains.
snug quest analytics --quest-id forge returns accepted / turned_in /
expired / abandoned counts and the turn-in rate (requires a platform
admin token).
Limits and configuration
Definitions are capped at 50 steps and 100000 total, event idempotency
keys live 24 hours, and puzzle regex patterns are bounded at 256
characters by default - tunable via the QUEST_* variables in the
Quest CONFIG reference.
Reference
- Quest API - every endpoint, callable
- Expeditions and puzzle chains - the other two quest kinds
- Related: Achievements for passive recognition, Treasury and Item Engine for reward sinks, Lottery for expedition outcome tables