This section covers the full configuration model: entity hierarchies, matching logic, assignment modes, and lifecycle management. It is relevant for anyone configuring missions through the dashboard or integrating them via API. For a high-level overview of what missions do, see the Gamification Fundamentals. For shared patterns used across all domains (JsonLogic expressions, timeframes, entity matching), see Cross-Cutting Patterns.
MissionConfiguration (what counts and how to count it)
│
└──▶ MissionRule (when, for whom, and how to assign)
│
└──▶ Mission (per-user or per-group tracking instance)
│
└──▶ MissionLog (immutable progress audit trail)| Field | Type | Description |
|---|---|---|
missionConfigurationId | nanoid | Unique identifier |
name | string | Human-readable reference (e.g., "Answer 10 Quizzes Correctly") |
missionType | INDIVIDUAL | GROUP | Whether missions track one user or a group |
matchType | INSTANCE | ENTITY | TAG | How to match incoming events |
matchEntity | Activity | Quiz | Tag | Which entity type to watch |
matchEntityId | string? | Specific entity or tag ID (required for INSTANCE and TAG) |
matchCondition | JsonLogic | Additional matching logic evaluated at runtime |
incrementExpression | JsonLogic | How much progress each matching event adds |
targetAmountExpression | JsonLogic | The completion threshold |
origin | CATALOG | CUSTOM | Where this configuration was created |
defaultLang | lang | Default language code |
langs | lang[] | Supported languages (1–10) |
INSTANCE: Matches a specific entity. Example: completing activity abc123. Requires matchEntityId.ENTITY: Matches any entity of that type. Example: completing any quiz.TAG: Matches any entity tagged with the given tag. Example: completing any activity tagged sustainability. Requires matchEntityId (the tag ID).matchCondition filters which events qualify. It receives { mission } as context and must return a truthy value for the event to count. Example: only count quizzes with outcome SUCCESS.incrementExpression defines how much progress each qualifying event adds. It receives { user, event } and must return a number. A static value like 1 means "add 1 per event". A conditional expression can award different amounts based on context — for instance, awarding 2 for hard quizzes and 1 for easy ones.targetAmountExpression defines when the mission is complete. It receives { user, mission } and must return a number. A static 10 means "complete after 10 increments". A dynamic expression can set different targets per user — for instance, a higher target for premium users.Constraint: When a JsonLogic expression evaluates to null, empty string, orNaN, the system defaults to1for both increment and target calculations.
| Field | Type | Description |
|---|---|---|
missionRuleId | nanoid | Unique identifier |
name | string | Human-readable name |
missionType | INDIVIDUAL | GROUP | Must match the referenced configurations |
state | PENDING | ACTIVE | ENDED | Lifecycle state |
assignmentMode | LAZY | EVENT | DISABLED | How missions are assigned to users |
usersMatchCondition | JsonLogic? | Which users this rule applies to (required for INDIVIDUAL) |
missionsMatchCondition | JsonLogic | Filters which configurations to instantiate |
missionConfigurationsPool | string[]? | Explicit list of configuration IDs to use (alternative to matching) |
LAZY: Missions are created on-demand when a user browses available missions. If the user matches the rule's conditions, the mission is generated in real time. Ideal for discovery-driven experiences where users choose which missions to pursue.EVENT: Missions are assigned automatically when a matching event occurs — for example, assigning a follow-up mission when the user completes a Learning Path. This is real-time and reactive.DISABLED: No assignments are made. Used for deactivating a rule without deleting it.assignmentMode is EVENT, four additional fields become required:| Field | Type | Description |
|---|---|---|
eventMatchType | INSTANCE | ENTITY | TAG | How to match the triggering event |
eventMatchEntity | Activity | Quiz | Tag | User | Which entity type triggers assignment |
eventMatchEntityId | string | Specific entity or tag ID |
eventMatchCondition | JsonLogic | Additional filtering on the event |
Constraint: All four eventMatch*fields are required whenassignmentModeisEVENTand must be absent for other modes.
usersMatchCondition determines which users are eligible for this rule. It receives { user, activeMissions } as context — where activeMissions is the list of missions already assigned to the user. This allows rules like "only assign if the user doesn't already have 3 active missions".Constraint: usersMatchConditionis required forINDIVIDUALrules and must be absent forGROUPrules (the entire group is eligible by definition).
missionsMatchCondition filters which Mission Configurations should be instantiated. It receives { user, activeMissions, mission } where mission is a candidate configuration. This allows rules like "only assign configurations tagged with the user's department".missionConfigurationsPool is an alternative to missionsMatchCondition — an explicit list of configuration IDs. When present, only these configurations are considered.| Field | Type | Description |
|---|---|---|
timeframeType | PERMANENT | RANGE | RECURRING | Whether the rule runs indefinitely, once, or repeats |
timeframeStartsAt | ISO datetime | When the rule begins |
timeframeEndsAt | ISO datetime? | When the rule ends (required for RANGE and RECURRING) |
timeframeTimezoneType | FIXED | USER | Whether to use a fixed timezone or each user's own |
timeframeTimezone | timezone? | The fixed timezone (required when FIXED) |
recurrence | DAILY | WEEKLY | MONTHLY | CUSTOM? | Reset cadence (required for RECURRING) |
scheduleCron | cron? | Cron expression (required when recurrence is CUSTOM) |
| Field | Type | Description |
|---|---|---|
groupTagId | string? | The tag that identifies the group (required for GROUP rules) |
Constraint: GROUPrules requiregroupTagId.INDIVIDUALrules must not have it.
| Field | Type | Description |
|---|---|---|
missionId | nanoid | Unique identifier |
missionConfigurationId | nanoid | The configuration this mission is based on |
missionRuleId | nanoid? | The rule that triggered this assignment |
missionType | INDIVIDUAL | GROUP | Inherited from configuration |
userId | nanoid? | The user this mission belongs to (INDIVIDUAL only) |
groupTagId | string? | The group this mission belongs to (GROUP only) |
state | PENDING | ACTIVE | ENDED | Lifecycle state |
isCompleted | boolean? | Whether the target has been reached |
completedAt | ISO datetime? | When the mission was completed |
currentAmount | number | Accumulated progress |
targetAmount | number | Frozen completion threshold |
periodId | string | Deduplication key for recurring missions |
matchType, matchEntity, matchEntityId, matchCondition, incrementExpression, targetAmountExpression) so that progress tracking does not depend on the configuration remaining unchanged.currentAmount >= targetAmount, the mission is marked as completed and stops accepting further increments.currentAmount. Unlike individual missions, group missions continue counting after reaching the target — they track cumulative group progress without capping.Constraint: INDIVIDUAL missions require userIdand forbidgroupTagId. GROUP missions requiregroupTagIdand forbiduserId.
periodId field serves as a deduplication key that prevents the same rule from assigning duplicate missions in the same time period:| Timeframe | periodId Format | Example |
|---|---|---|
PERMANENT | "PERMANENT" | PERMANENT |
RANGE | Rule's start time in UTC | 2025-01-01T00:00:00 |
RECURRING / DAILY | YYYY-MM-DD | 2025-09-15 |
RECURRING / WEEKLY | YYYY-Www | 2025-W38 |
RECURRING / MONTHLY | YYYY-MM | 2025-09 |
RECURRING / CUSTOM | Cron last-fire time in UTC | 2025-09-15T06:00:00 |
rule assigns mission
│
┌──────▼──────┐
│ PENDING │ ◀── mission starts in the future
└──────┬──────┘
│ startsAt reached
▼
┌─────────────┐ currentAmount >= targetAmount
│ ACTIVE │ ──────────────────────────────────▶ isCompleted = true
└──────┬──────┘ (INDIVIDUAL only; GROUP keeps counting)
│ endsAt reached
▼
┌─────────────┐
│ ENDED │ ◀── timeframe expired
└─────────────┘targetAmount is not yet calculated.targetAmount is calculated from targetAmountExpression and frozen — subsequent changes to the expression or user context do not affect it.| Field | Type | Description |
|---|---|---|
missionLogId | nanoid | Unique identifier |
missionId | nanoid | The mission that was updated |
missionConfigurationId | nanoid | The configuration reference |
missionType | INDIVIDUAL | GROUP | Mission type |
userId | nanoid | The user who triggered the progress |
groupTagId | nanoid? | For group missions |
amount | number | The increment applied (default: 1) |
additionalData | record? | Extra context from the source event |
userId records which user in the group contributed, while the increment applies to the shared currentAmount.ACTIVE rules with assignmentMode: EVENT that match the event's entity type and ID.eventMatchCondition is evaluated against the event data.usersMatchCondition is evaluated against the user (for INDIVIDUAL) or skipped (for GROUP).missionsMatchCondition (or missionConfigurationsPool) determines which configurations to instantiate.MissionRuleEvaluation check prevents duplicate assignments in the same period.ACTIVE rules with assignmentMode: LAZY.| Source Event | Maps To |
|---|---|
ActivityLog | Activity |
QuizLog | Quiz |
| Others | Pass through |
ACTIVE, non-completed missions that match the event (by matchType, matchEntity, matchEntityId).matchCondition is evaluated against the event context.incrementExpression is evaluated to determine how much to add.currentAmount is atomically incremented.MissionLog entry is created.currentAmount >= targetAmount, the mission is marked as completed (isCompleted: true, completedAt set).currentAmount is only incremented once. This is enforced via an idempotency key based on the event ID.isCompleted: true if it was previously false, preventing double-completion.{
"missionConfigurationId": "mc_quiz_weekly",
"name": "Weekly Quiz Challenge",
"missionType": "INDIVIDUAL",
"matchType": "ENTITY",
"matchEntity": "Quiz",
"matchCondition": { "===": [{ "var": "event.outcome" }, "SUCCESS"] },
"incrementExpression": 1,
"targetAmountExpression": 5,
"defaultLang": "en",
"langs": ["en", "it"]
}{
"missionRuleId": "mr_quiz_weekly",
"name": "Weekly Quiz Rule",
"missionType": "INDIVIDUAL",
"assignmentMode": "LAZY",
"usersMatchCondition": true,
"missionsMatchCondition": true,
"missionConfigurationsPool": ["mc_quiz_weekly"],
"timeframeType": "RECURRING",
"timeframeStartsAt": "2025-01-06T00:00:00Z",
"timeframeEndsAt": "2025-12-31T23:59:59Z",
"timeframeTimezoneType": "USER",
"recurrence": "WEEKLY",
"defaultLang": "en",
"langs": ["en"]
}periodId: "2025-W38", state: "ACTIVE", currentAmount: 0, targetAmount: 5matchCondition passes. incrementExpression returns 1. Mission becomes currentAmount: 1.matchCondition evaluates to false (outcome is not SUCCESS). No increment.currentAmount: 5, isCompleted: true.2025-W39). The LAZY rule creates a fresh mission with currentAmount: 0.{
"missionRuleId": "mr_team_event",
"name": "Team Onboarding Challenge",
"missionType": "GROUP",
"groupTagId": "department:engineering",
"assignmentMode": "EVENT",
"eventMatchType": "ENTITY",
"eventMatchEntity": "Activity",
"eventMatchEntityId": "activity_onboarding",
"eventMatchCondition": true,
"missionsMatchCondition": true,
"missionConfigurationsPool": ["mc_team_onboarding"],
"timeframeType": "RANGE",
"timeframeStartsAt": "2025-09-01T00:00:00Z",
"timeframeEndsAt": "2025-09-30T23:59:59Z",
"timeframeTimezoneType": "FIXED",
"timeframeTimezone": "Europe/Rome"
}| Concept | Purpose |
|---|---|
| MissionConfiguration | Defines what events count and how to measure progress (matching + expressions) |
| MissionRule | Defines when, for whom, and how missions are assigned (timeframe + targeting + mode) |
| Mission | Per-user or per-group tracking instance with frozen target and live progress |
| MissionLog | Immutable audit trail of every progress event |
| assignmentMode | How missions are created: LAZY (on-demand), EVENT (reactive), DISABLED (off) |
| missionType | INDIVIDUAL (one user, stops at target) or GROUP (shared counter, keeps going) |
| matchType | How events are matched: INSTANCE (specific), ENTITY (any of type), TAG (by tag) |
| periodId | Deduplication key ensuring one mission per rule per time period |
| targetAmount | Frozen when mission becomes ACTIVE — immune to later configuration changes |
| MissionRuleEvaluation | Tracks which rule+period combinations have been evaluated, preventing duplicates |