All developer documentation

Daily challenges

Games do not have to have dailies. This is a capability, not an obligation — but if your game has something a player could finish today and finish again tomorrow, the platform can hold that state, count the streak, and put a marker on your game's card in the arcade so a player scanning the catalog sees there is something waiting in it.

That marker is the point. A daily challenge is not primarily a reward mechanism — it does not pay, and section 5 is emphatic about that — it is a reason to come back.

A daily challenge is a progression. Not a new kind of object, not its own config section: a progression that repeats on a known period and asks to be shown. Everything you already know about progressions applies, and inventory-and-catalogs.md is still where progressions are explained.

1. Is this the mechanism you want#

Two things look alike and are not. Pick with the table, because the choice decides where the state lives, and a row in the wrong place is not recoverable by reading it back.

You wantUseBecause
"Clear 10 piles today", shown in the arcade, streakeda period schedule + challengeyesterday survives; a missed day is not a zero
A repeatable reward that pays outa cron reseta challenge cannot pay; a cron progression can
A season track, a battle passan ordinary progressionit accumulates; it does not reset
An internal counter you do not want showna period schedule, no challengethe platform stores it and displays nothing

The difference between the first two is not cosmetic. A cron reset is destructive: it zeroes one lifetime row in place, so yesterday is overwritten. There is no calendar to compute a streak from, no way to tell "missed it" from "scored zero", and no completion history. A period schedule keeps one row per period and never overwrites.

So cron is the right answer for a repeatable reward and the wrong one for a challenge. If you find yourself wanting both — a challenge that pays — read section 5 before you design around it.

2. Declare it in your game config#

Two fields on an ordinary progression:

progressions:
  - id: 4f2a9c71-3e8b-4d15-9a62-7c0e5b1d8f43
    slug: clear-ten-piles
    display_name: "Clear ten piles"
    description: "Clear ten piles in a single run."
    config:
      completion_threshold: 10
      reset_schedule: { type: daily } # daily | weekly | monthly
      platform_display: challenge # none (the default) | challenge

reset_schedule is the whole of the scheduling — there is no cron expression to write and no timezone to choose. platform_display: challenge is what makes it the arcade's business rather than only yours; the default is none, and a progression on a daily schedule that says nothing appears on no platform surface at all.

completion_threshold is the target. client_writable decides who may report it, and section 4 is where that choice actually matters.

Upload the config the way you upload any other. A challenge is not promoted differently and does not need a new build to reach players.

3. Report progress from your game#

The write is the ordinary progression write. Your game already knows how to make it:

await gem.me.mutations.addProgression('clear-ten-piles', 1); // a counter
await gem.me.mutations.maxProgression('best-depth-today', 7); // a record

addProgression accumulates and maxProgression keeps the best, exactly as they do for any other progression. The platform routes the write to the current period on its own — your game does not name a period, compute one, or pass a date.

A progression on a period schedule writes a period row and NO lifetime row. This is the one runtime surprise, and it is worth stating plainly because the symptom is silence:

await gem.me.mutations.addProgression('clear-ten-piles', 10);
const row = await gem.me.progressions.get('clear-ten-piles'); // null

The read is not broken and the write did not fail. Period state lives apart from lifetime progression state, and the ordinary progression read only ever sees the latter. Do not add a retry, and do not call progressionSet to "fix" the missing row — a set writes the lifetime row you were not supposed to have, and now the player has state in two places that mean different things.

4. Who may report it#

client_writable decides whether the browser may make that write or whether it must come from your game's server.

A browser credential is public. A client_writable challenge is therefore forgeable: a player can complete today's challenge without playing. That is allowed, and the platform accepts it, because a challenge cannot pay anything — forging one moves a marker on a card and nothing else.

Choose server-reported when the challenge is a claim about skill you intend to stand behind — a leaderboard-adjacent "beat this in under a minute" — and client-reported when it is a nudge to play. If your game has no server, client-reported is the only option, and for a challenge that is a reasonable place to be.

5. A challenge does not pay#

A progression on a daily, weekly or monthly schedule cannot grant anything, and config that asks it to is refused at upload — not accepted and quietly ignored.

Four fields are rejected on a period schedule:

FieldWhy it is refused
rewards_on_completethe period path emits no inventory changes, so the operations would never run
platform_grantsa grant is derived from those same changes, so nothing would ever be granted
trackstrack claim state lives on the lifetime row a period-scoped progression has not
max_completionsit is a lifetime cap, and period rows do not accumulate into one

The refusal is deliberate and the alternative is worse: a silent accept would let you ship a challenge that uploads clean, looks correct in your config, and pays nobody for months before anyone notices.

So grant from your own game server. Your game reports the completion to the platform for the marker and the streak, and pays the player through whatever it already uses to pay them — a game currency, an item, an unlock. If the progression itself must pay through the platform, it is not a challenge: use a cron reset and accept that you lose the calendar with it.

6. Periods are UTC, and your game must not compute the boundary#

A day ends at 00:00 UTC for every player, everywhere. A player at UTC+13 sees the day roll over at 11:00 their time; a player at UTC-8 at 16:00 theirs.

Two consequences, and the second is the one that breaks code:

  • Do not word a challenge as "today". "Today's board" is a fine title. A mechanic that assumes the player's local midnight is the boundary is wrong for most of the planet.

  • Never derive the boundary from a clock. A period is identified by an instant the platform supplies, not by a date your game computes. A game that asks the platform when the current period ends keeps working if the platform's periods ever change shape; a game that computed one does not.

7. An id is permanent, and means one thing forever#

A challenge's id is carried on every player's history for a long time, with nothing pointing back at your config to explain it. Reusing an id for a different challenge silently merges two challenges into one history and makes every number computed from it unexplainable — not wrong in a way that shows, just quietly meaningless.

  • A new challenge gets a new id. Always.

  • To rename what the player sees, change display_name. That is free and is not part of the challenge's identity.

  • Upload refuses a config that changes an id's period, checked against your recent versions.

8. Retuning the target mid-period is safe#

completion_threshold is snapshotted onto each player's row at their first progress in a period. Raising it later cannot un-complete a period a player already finished, and cannot rewrite a historical percentage.

Lowering it mid-period is safe too, and has a consequence worth knowing: a player who already started that period keeps the target they started with. Two players in the same period can be working toward different numbers, and every platform surface shows each player their own. That is deliberate — the alternative rewrites history — but it will generate a support question the first time two friends compare.

9. Streaks#

The platform counts consecutive periods in which a player completed what your game offered, and keeps a longest-ever alongside the current one. Your game declares nothing to get this; it follows from the challenge.

Two things to know before you build a mechanic on it:

  • A streak is the platform's, and it is shown on platform surfaces. Your game reports progress and does not read the streak back — see below.

  • Whether a player's streak is shown at all can depend on who they are. Some players do not see streak surfaces. Do not design a mechanic whose fairness depends on every player seeing one.

10. What your game can read back#

Your game reports daily progress; it does not read it back. The ordinary progression read returns nothing for a period-scoped progression (section 3), and the SDK surfaces no period or streak read of its own.

That is workable because the arcade is what displays the challenge state — the marker on your game's card, and the player's own progress and streak on platform surfaces. Your game does not have to render any of it.

If your game wants its own in-game daily UI, render what it just reported. Your game knows what it sent this session. Treat that as a view of this session's play, not as the authoritative daily record — the platform holds that, and it is the one that survives the player switching devices.

11. When an upload is refused#

What you getWhat happenedWhat to do
rewards_on_complete cannot be combinedyou gave a challenge a rewardgrant from your game server; see section 5
platform_grants cannot be combinedyou mapped a challenge onto a platform currencysame; or use a cron reset if the progression must pay
tracks cannot be combinedyou gave a challenge tiersone target per period, or use a cron reset
max_completions cannot be combinedyou capped a challenge's lifetime completionsremove it; periods are not capped this way
platform_display: challenge needs a periodyou asked to display a progression that never resetsadd a daily, weekly or monthly reset_schedule
an id's period changedyou edited an existing challenge's scheduleleave it and add a new challenge with a new id

Every refusal names the field and the reason. Read the error rather than a list written down here — the platform is what decides these, and this table is only as current as the day it was written.

Read this page as Markdown